Compare commits

..

4 Commits

Author SHA1 Message Date
506c8682fe umami
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-29 17:43:06 +01:00
a909de30f3 filter products
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 3m52s
2026-01-29 17:34:15 +01:00
a2f94f15bc og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m48s
2026-01-29 17:26:02 +01:00
13e56a88bc headings
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m3s
2026-01-29 16:51:11 +01:00
21 changed files with 144 additions and 68 deletions

View File

@@ -1,10 +1,11 @@
import { notFound } from 'next/navigation';
import { MDXRemote } from 'next-mdx-remote/rsc';
import { Container, Badge } from '@/components/ui';
import { Container, Badge, Heading } from '@/components/ui';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getPageBySlug, getAllPages } from '@/lib/pages';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { getOGImageMetadata } from '@/lib/metadata';
interface PageProps {
params: {
@@ -47,6 +48,7 @@ export async function generateMetadata({ params: { locale, slug } }: PageProps):
title: `${pageData.frontmatter.title} | KLZ Cables`,
description: pageData.frontmatter.excerpt || '',
url: `https://klz-cables.com/${locale}/${slug}`,
images: getOGImageMetadata(slug, pageData.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -74,9 +76,9 @@ export default async function StandardPage({ params: { locale, slug } }: PagePro
<Container className="relative z-10">
<div className="max-w-4xl animate-slide-up">
<Badge variant="accent" className="mb-4 md:mb-6">{t('badge')}</Badge>
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-0 leading-tight">
<Heading level={1} className="text-white mb-0">
{pageData.frontmatter.title}
</h1>
</Heading>
</div>
</Container>
</section>

View File

@@ -50,10 +50,11 @@ export async function GET(
);
}
const { origin } = new URL(request.url);
const featuredImage = product.frontmatter.images?.[0]
? (product.frontmatter.images[0].startsWith('http')
? product.frontmatter.images[0]
: `https://klz-cables.com${product.frontmatter.images[0]}`)
: `${origin}${product.frontmatter.images[0]}`)
: undefined;
return new ImageResponse(

View File

@@ -10,6 +10,8 @@ import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents';
import { mdxComponents } from '@/components/blog/MDXComponents';
import { Heading } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogPostProps {
params: {
@@ -42,6 +44,7 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
publishedTime: post.frontmatter.date,
authors: ['KLZ Cables'],
url: `https://klz-cables.com/${locale}/blog/${slug}`,
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -84,9 +87,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</div>
)}
<h1 className="text-4xl md:text-5xl lg:text-6xl font-bold text-white mb-8 leading-[1.1] drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
<Heading level={1} className="text-white mb-8 drop-shadow-2xl animate-slight-fade-in-from-bottom [animation-delay:200ms]">
{post.frontmatter.title}
</h1>
</Heading>
<div className="flex flex-wrap items-center gap-6 text-white/80 text-sm md:text-base font-medium animate-slight-fade-in-from-bottom [animation-delay:400ms]">
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {
@@ -112,9 +115,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span>
</div>
)}
<h1 className="text-4xl md:text-5xl font-bold text-text-primary mb-8 leading-tight">
<Heading level={1} className="mb-8">
{post.frontmatter.title}
</h1>
</Heading>
<div className="flex items-center gap-6 text-text-secondary font-medium">
<time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, {

View File

@@ -3,6 +3,7 @@ import { getAllPosts } from '@/lib/blog';
import { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogIndexProps {
params: {
@@ -27,6 +28,7 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
title: `${t('title')} | KLZ Cables`,
description: t('description'),
url: `https://klz-cables.com/${locale}/blog`,
images: getOGImageMetadata('blog', t('title'), locale),
},
twitter: {
card: 'summary_large_image',
@@ -69,9 +71,9 @@ export default async function BlogIndex({ params: { locale } }: BlogIndexProps)
<Badge variant="saturated" className="mb-4 md:mb-6">{t('featuredPost')}</Badge>
{featuredPost && (
<>
<h1 className="text-3xl md:text-6xl font-extrabold text-white mb-4 md:mb-8 leading-[1.1] line-clamp-3 md:line-clamp-none">
<Heading level={1} className="text-white mb-4 md:mb-8">
{featuredPost.frontmatter.title}
</h1>
</Heading>
<p className="text-base md:text-xl text-white/80 mb-6 md:mb-10 line-clamp-2 md:line-clamp-2 max-w-2xl">
{featuredPost.frontmatter.excerpt}
</p>

View File

@@ -4,6 +4,8 @@ import Reveal from '@/components/Reveal';
import { Container, Heading, Section } from '@/components/ui';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { SITE_URL } from '@/lib/schema';
import { getOGImageMetadata } from '@/lib/metadata';
import { Suspense } from 'react';
import dynamic from 'next/dynamic';
const LeafletMap = dynamic(() => import('@/components/LeafletMap'), {
@@ -40,14 +42,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
description,
url: `https://klz-cables.com/${locale}/contact`,
siteName: 'KLZ Cables',
images: [
{
url: 'https://klz-cables.com/logo.png',
width: 1200,
height: 630,
alt: 'KLZ Cables Contact',
},
],
images: getOGImageMetadata('contact', title, locale),
locale: `${locale.toUpperCase()}_DE`,
type: 'website',
},
@@ -55,7 +50,7 @@ export async function generateMetadata({ params: { locale } }: ContactPageProps)
card: 'summary_large_image',
title: `${title} | KLZ Cables`,
description,
images: ['https://klz-cables.com/logo.png'],
images: [`${SITE_URL}/${locale}/contact/opengraph-image`],
},
robots: {
index: true,

View File

@@ -2,7 +2,7 @@ import { ImageResponse } from 'next/og';
import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate';
export const runtime = 'edge';
export const runtime = 'nodejs';
export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations({ locale, namespace: 'Index.meta' });

View File

@@ -1,6 +1,6 @@
import Hero from '@/components/home/Hero';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema } from '@/lib/schema';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import ProductCategories from '@/components/home/ProductCategories';
import WhatWeDo from '@/components/home/WhatWeDo';
import RecentPosts from '@/components/home/RecentPosts';
@@ -13,6 +13,7 @@ import CTA from '@/components/home/CTA';
import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import { getOGImageMetadata } from '@/lib/metadata';
export default function HomePage({ params: { locale } }: { params: { locale: string } }) {
return (
@@ -70,6 +71,7 @@ export async function generateMetadata({ params: { locale } }: { params: { local
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}`,
images: getOGImageMetadata('', title, locale),
},
twitter: {
card: 'summary_large_image',

View File

@@ -5,12 +5,13 @@ import ProductSidebar from '@/components/ProductSidebar';
import ProductTabs from '@/components/ProductTabs';
import ProductTechnicalData from '@/components/ProductTechnicalData';
import RelatedProducts from '@/components/RelatedProducts';
import { Badge, Container, Section } from '@/components/ui';
import { Badge, Container, Heading, Section } from '@/components/ui';
import { getDatasheetPath } from '@/lib/datasheets';
import { getAllProducts, getProductBySlug } from '@/lib/mdx';
import { mapFileSlugToTranslated, mapSlugToFileSlug } from '@/lib/slugs';
import { Metadata } from 'next';
import { getTranslations } from 'next-intl/server';
import { getProductOGImageMetadata } from '@/lib/metadata';
import { MDXRemote } from 'next-mdx-remote/rsc';
import Image from 'next/image';
import Link from 'next/link';
@@ -51,14 +52,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
title: `${categoryTitle} | KLZ Cables`,
description: categoryDesc,
url: `https://klz-cables.com/${locale}/products/${productSlug}`,
images: [
{
url: `/api/og/product?slug=${fileSlug}`,
width: 1200,
height: 630,
alt: categoryTitle,
},
],
images: getProductOGImageMetadata(fileSlug, categoryTitle, locale),
},
twitter: {
card: 'summary_large_image',
@@ -87,14 +81,7 @@ export async function generateMetadata({ params }: ProductPageProps): Promise<Me
description: product.frontmatter.description,
type: 'website',
url: `https://klz-cables.com/${locale}/products/${slug.join('/')}`,
images: [
{
url: `/api/og/product?slug=${productSlug}`,
width: 1200,
height: 630,
alt: product.frontmatter.title,
},
],
images: getProductOGImageMetadata(productSlug, product.frontmatter.title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -180,9 +167,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
<span className="mx-3 opacity-30">/</span>
<span className="text-white/90">{categoryTitle}</span>
</nav>
<h1 className="text-5xl md:text-7xl lg:text-8xl font-extrabold text-white mb-8 tracking-tight leading-[1.05]">
<Heading level={1} className="text-white mb-8">
{categoryTitle}
</h1>
</Heading>
<div className="h-1.5 w-24 bg-accent rounded-full" />
</div>
</Container>
@@ -325,9 +312,9 @@ export default async function ProductPage({ params }: ProductPageProps) {
</Badge>
))}
</div>
<h1 className="text-6xl md:text-8xl lg:text-9xl font-black text-white mb-8 tracking-tighter leading-[0.9] uppercase">
<Heading level={1} className="text-white mb-8 uppercase">
{product.frontmatter.title}
</h1>
</Heading>
<p className="text-xl md:text-2xl text-white/60 max-w-2xl leading-relaxed font-medium">
{product.frontmatter.description}
</p>

View File

@@ -1,11 +1,12 @@
import Reveal from '@/components/Reveal';
import Scribble from '@/components/Scribble';
import { Badge, Button, Card, Container, Section } from '@/components/ui';
import { Badge, Button, Card, Container, Heading, Section } from '@/components/ui';
import { getTranslations } from 'next-intl/server';
import { Metadata } from 'next';
import Image from 'next/image';
import Link from 'next/link';
import { mapFileSlugToTranslated } from '@/lib/slugs';
import { getOGImageMetadata } from '@/lib/metadata';
interface ProductsPageProps {
params: {
@@ -32,6 +33,7 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/products`,
images: getOGImageMetadata('products', title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -90,7 +92,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5">
{t('heroSubtitle')}
</Badge>
<h1 className="text-4xl md:text-6xl lg:text-7xl font-extrabold text-white mb-4 md:mb-8 tracking-tight leading-[1.05]">
<Heading level={1} className="text-white mb-4 md:mb-8">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">
@@ -99,7 +101,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
</span>
)
})}
</h1>
</Heading>
<p className="text-lg md:text-xl text-white/70 leading-relaxed max-w-2xl mb-8 md:mb-12 line-clamp-2 md:line-clamp-none">
{t('subtitle')}
</p>

View File

@@ -3,6 +3,7 @@ import { Metadata } from 'next';
import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import { Section, Container, Heading, Badge, Button } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
import Image from 'next/image';
import Reveal from '@/components/Reveal';
import Gallery from '@/components/team/Gallery';
@@ -32,6 +33,7 @@ export async function generateMetadata({ params: { locale } }: TeamPageProps): P
title: `${title} | KLZ Cables`,
description,
url: `https://klz-cables.com/${locale}/team`,
images: getOGImageMetadata('team', title, locale),
},
twitter: {
card: 'summary_large_image',
@@ -102,9 +104,9 @@ export default async function TeamPage({ params: { locale } }: TeamPageProps) {
<Container className="relative z-10 text-center text-white max-w-5xl">
<Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg">{t('hero.badge')}</Badge>
<h1 className="text-3xl md:text-6xl lg:text-7xl font-extrabold tracking-tight leading-[1.1] mb-4 md:mb-8">
<Heading level={1} className="text-white mb-4 md:mb-8">
{t('hero.subtitle')}
</h1>
</Heading>
<p className="text-lg md:text-2xl text-white/70 font-medium italic">
{t('hero.title')}
</p>

View File

@@ -46,6 +46,8 @@ export function OGImageTemplate({
<img
src={image}
alt=""
width="1200"
height="630"
style={{
width: '100%',
height: '100%',

View File

@@ -49,6 +49,7 @@ export default function AnalyticsProvider() {
id="umami-analytics"
src="/stats/script.js"
data-website-id={websiteId}
data-host-url="/stats"
strategy="afterInteractive"
data-domains="klz-cables.com"
defer

View File

@@ -19,7 +19,7 @@ export default function Hero() {
variants={containerVariants}
>
<motion.div variants={headingVariants}>
<Heading level={1} className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]">
<Heading level={1} className="text-center md:text-left mb-6 md:mb-8 md:max-w-none text-white text-4xl sm:text-5xl md:text-7xl font-extrabold [text-shadow:_-2px_-2px_0_#002b49,_2px_-2px_0_#002b49,_-2px_2px_0_#002b49,_2px_2px_0_#002b49,_-2px_0_0_#002b49,_2px_0_0_#002b49,_0_-2px_0_#002b49,_0_2px_0_#002b49]">
{t.rich('title', {
green: (chunks) => (
<span className="relative inline-block">

View File

@@ -17,9 +17,9 @@ export function Heading({
const Tag = `h${level}` as any;
const sizes = {
1: 'text-3xl md:text-5xl lg:text-6xl font-extrabold leading-[1.1] tracking-tight',
2: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.2] tracking-tight',
3: 'text-xl md:text-2xl lg:text-3xl font-bold leading-[1.3] tracking-tight',
1: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.1] tracking-tight',
2: 'text-xl md:text-3xl lg:text-4xl font-bold leading-[1.2] tracking-tight',
3: 'text-lg md:text-2xl lg:text-3xl font-bold leading-[1.3] tracking-tight',
4: 'text-lg md:text-xl lg:text-2xl font-bold leading-[1.4]',
5: 'text-base md:text-lg font-bold leading-[1.5]',
6: 'text-base md:text-lg font-semibold leading-[1.6]',

View File

@@ -31,6 +31,8 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
filePath = path.join(productsDir, `${fileSlug}-2.mdx`);
}
let product: ProductMdx | null = null;
if (!fs.existsSync(filePath)) {
// Fallback to English if locale is not 'en'
if (locale !== 'en') {
@@ -43,7 +45,7 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
if (fs.existsSync(enFilePath)) {
const fileContent = fs.readFileSync(enFilePath, 'utf8');
const { data, content } = matter(fileContent);
return {
product = {
slug: fileSlug,
frontmatter: {
...data,
@@ -53,17 +55,23 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
};
}
}
} else {
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
product = {
slug: fileSlug,
frontmatter: data as ProductFrontmatter,
content,
};
}
// Filter out products without images
if (product && (!product.frontmatter.images || product.frontmatter.images.length === 0 || !product.frontmatter.images[0])) {
return null;
}
const fileContent = fs.readFileSync(filePath, 'utf8');
const { data, content } = matter(fileContent);
return {
slug: fileSlug,
frontmatter: data as ProductFrontmatter,
content,
};
return product;
}
export async function getAllProductSlugs(locale: string): Promise<string[]> {

24
lib/metadata.ts Normal file
View File

@@ -0,0 +1,24 @@
import { Metadata } from 'next';
import { SITE_URL } from './schema';
export function getOGImageMetadata(path: string, title: string, locale: string): NonNullable<Metadata['openGraph']>['images'] {
return [
{
url: `${SITE_URL}/${locale}/${path}/opengraph-image`,
width: 1200,
height: 630,
alt: title,
},
];
}
export function getProductOGImageMetadata(slug: string, title: string, locale: string): NonNullable<Metadata['openGraph']>['images'] {
return [
{
url: `${SITE_URL}/${locale}/api/og/product?slug=${slug}`,
width: 1200,
height: 630,
alt: title,
},
];
}

View File

@@ -63,6 +63,7 @@
"lint": "next lint",
"typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests",
"test:og": "vitest run tests/og-image.test.ts",
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts"
},

View File

@@ -6,8 +6,6 @@ Sentry.init({
dsn,
enabled: Boolean(dsn),
tracesSampleRate: 0,
// Ensure 500 errors are always captured
debug: process.env.NODE_ENV === 'development',
// AdjustingreplaysOnErrorSampleRate to 1.0 to capture 100% of errors with replays if enabled
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.1,

View File

@@ -6,7 +6,5 @@ Sentry.init({
dsn,
enabled: Boolean(dsn),
tracesSampleRate: 0,
// Ensure 500 errors are always captured
debug: process.env.NODE_ENV === 'development',
});

View File

@@ -6,9 +6,5 @@ Sentry.init({
dsn,
enabled: Boolean(dsn),
tracesSampleRate: 0,
// Ensure 500 errors are always captured
// Next.js 14+ with App Router handles many errors automatically,
// but we want to be explicit about capturing all unhandled exceptions.
debug: process.env.NODE_ENV === 'development',
});

52
tests/og-image.test.ts Normal file
View File

@@ -0,0 +1,52 @@
import { describe, it, expect } from 'vitest';
const BASE_URL = process.env.TEST_URL || 'http://localhost:3000';
describe('OG Image Generation', () => {
const locales = ['de', 'en'];
const productSlugs = ['nay2y']; // Based on data/products/de/nay2y.mdx
async function verifyImageResponse(response: Response) {
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toContain('image/png');
const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer);
// Check for PNG signature: 89 50 4E 47 0D 0A 1A 0A
expect(bytes[0]).toBe(0x89);
expect(bytes[1]).toBe(0x50);
expect(bytes[2]).toBe(0x4E);
expect(bytes[3]).toBe(0x47);
// Check that the image is not empty and has a reasonable size
// A 1200x630 OG image should be at least 4KB
expect(bytes.length).toBeGreaterThan(4000);
}
locales.forEach((locale) => {
it(`should generate main OG image for ${locale}`, async () => {
const url = `${BASE_URL}/${locale}/opengraph-image`;
const response = await fetch(url);
await verifyImageResponse(response);
}, 30000);
it(`should generate product OG image for ${locale} with slug ${productSlugs[0]}`, async () => {
const url = `${BASE_URL}/${locale}/api/og/product?slug=${productSlugs[0]}`;
const response = await fetch(url);
await verifyImageResponse(response);
}, 30000);
it(`should return 400 for product OG image without slug in ${locale}`, async () => {
const url = `${BASE_URL}/${locale}/api/og/product`;
const response = await fetch(url);
expect(response.status).toBe(400);
}, 30000);
});
it('should generate blog OG image', async () => {
const url = `${BASE_URL}/de/blog/opengraph-image`;
const response = await fetch(url);
await verifyImageResponse(response);
}, 30000);
});