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

View File

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

View File

@@ -10,6 +10,8 @@ import PostNavigation from '@/components/blog/PostNavigation';
import PowerCTA from '@/components/blog/PowerCTA'; import PowerCTA from '@/components/blog/PowerCTA';
import TableOfContents from '@/components/blog/TableOfContents'; import TableOfContents from '@/components/blog/TableOfContents';
import { mdxComponents } from '@/components/blog/MDXComponents'; import { mdxComponents } from '@/components/blog/MDXComponents';
import { Heading } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogPostProps { interface BlogPostProps {
params: { params: {
@@ -42,6 +44,7 @@ export async function generateMetadata({ params: { locale, slug } }: BlogPostPro
publishedTime: post.frontmatter.date, publishedTime: post.frontmatter.date,
authors: ['KLZ Cables'], authors: ['KLZ Cables'],
url: `https://klz-cables.com/${locale}/blog/${slug}`, url: `https://klz-cables.com/${locale}/blog/${slug}`,
images: getOGImageMetadata(`blog/${slug}`, post.frontmatter.title, locale),
}, },
twitter: { twitter: {
card: 'summary_large_image', card: 'summary_large_image',
@@ -84,9 +87,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span> </span>
</div> </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} {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]"> <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}> <time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, { {new Date(post.frontmatter.date).toLocaleDateString(locale, {
@@ -112,9 +115,9 @@ export default async function BlogPost({ params: { locale, slug } }: BlogPostPro
</span> </span>
</div> </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} {post.frontmatter.title}
</h1> </Heading>
<div className="flex items-center gap-6 text-text-secondary font-medium"> <div className="flex items-center gap-6 text-text-secondary font-medium">
<time dateTime={post.frontmatter.date}> <time dateTime={post.frontmatter.date}>
{new Date(post.frontmatter.date).toLocaleDateString(locale, { {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 { Section, Container, Heading, Card, Badge, Button } from '@/components/ui';
import Reveal from '@/components/Reveal'; import Reveal from '@/components/Reveal';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { getOGImageMetadata } from '@/lib/metadata';
interface BlogIndexProps { interface BlogIndexProps {
params: { params: {
@@ -27,6 +28,7 @@ export async function generateMetadata({ params: { locale } }: BlogIndexProps) {
title: `${t('title')} | KLZ Cables`, title: `${t('title')} | KLZ Cables`,
description: t('description'), description: t('description'),
url: `https://klz-cables.com/${locale}/blog`, url: `https://klz-cables.com/${locale}/blog`,
images: getOGImageMetadata('blog', t('title'), locale),
}, },
twitter: { twitter: {
card: 'summary_large_image', 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> <Badge variant="saturated" className="mb-4 md:mb-6">{t('featuredPost')}</Badge>
{featuredPost && ( {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} {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"> <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} {featuredPost.frontmatter.excerpt}
</p> </p>

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,11 +1,12 @@
import Reveal from '@/components/Reveal'; import Reveal from '@/components/Reveal';
import Scribble from '@/components/Scribble'; 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 { getTranslations } from 'next-intl/server';
import { Metadata } from 'next'; import { Metadata } from 'next';
import Image from 'next/image'; import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { mapFileSlugToTranslated } from '@/lib/slugs'; import { mapFileSlugToTranslated } from '@/lib/slugs';
import { getOGImageMetadata } from '@/lib/metadata';
interface ProductsPageProps { interface ProductsPageProps {
params: { params: {
@@ -32,6 +33,7 @@ export async function generateMetadata({ params: { locale } }: ProductsPageProps
title: `${title} | KLZ Cables`, title: `${title} | KLZ Cables`,
description, description,
url: `https://klz-cables.com/${locale}/products`, url: `https://klz-cables.com/${locale}/products`,
images: getOGImageMetadata('products', title, locale),
}, },
twitter: { twitter: {
card: 'summary_large_image', 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"> <Badge variant="saturated" className="mb-4 md:mb-8 shadow-lg px-3 py-1 md:px-4 md:py-1.5">
{t('heroSubtitle')} {t('heroSubtitle')}
</Badge> </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', { {t.rich('title', {
green: (chunks) => ( green: (chunks) => (
<span className="relative inline-block"> <span className="relative inline-block">
@@ -99,7 +101,7 @@ export default async function ProductsPage({ params }: ProductsPageProps) {
</span> </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"> <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')} {t('subtitle')}
</p> </p>

View File

@@ -3,6 +3,7 @@ import { Metadata } from 'next';
import JsonLd from '@/components/JsonLd'; import JsonLd from '@/components/JsonLd';
import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema'; import { getBreadcrumbSchema, SITE_URL } from '@/lib/schema';
import { Section, Container, Heading, Badge, Button } from '@/components/ui'; import { Section, Container, Heading, Badge, Button } from '@/components/ui';
import { getOGImageMetadata } from '@/lib/metadata';
import Image from 'next/image'; import Image from 'next/image';
import Reveal from '@/components/Reveal'; import Reveal from '@/components/Reveal';
import Gallery from '@/components/team/Gallery'; import Gallery from '@/components/team/Gallery';
@@ -32,6 +33,7 @@ export async function generateMetadata({ params: { locale } }: TeamPageProps): P
title: `${title} | KLZ Cables`, title: `${title} | KLZ Cables`,
description, description,
url: `https://klz-cables.com/${locale}/team`, url: `https://klz-cables.com/${locale}/team`,
images: getOGImageMetadata('team', title, locale),
}, },
twitter: { twitter: {
card: 'summary_large_image', 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"> <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> <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')} {t('hero.subtitle')}
</h1> </Heading>
<p className="text-lg md:text-2xl text-white/70 font-medium italic"> <p className="text-lg md:text-2xl text-white/70 font-medium italic">
{t('hero.title')} {t('hero.title')}
</p> </p>

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ export default function Hero() {
variants={containerVariants} variants={containerVariants}
> >
<motion.div variants={headingVariants}> <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', { {t.rich('title', {
green: (chunks) => ( green: (chunks) => (
<span className="relative inline-block"> <span className="relative inline-block">

View File

@@ -17,9 +17,9 @@ export function Heading({
const Tag = `h${level}` as any; const Tag = `h${level}` as any;
const sizes = { const sizes = {
1: 'text-3xl md:text-5xl lg:text-6xl font-extrabold leading-[1.1] tracking-tight', 1: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.1] tracking-tight',
2: 'text-2xl md:text-4xl lg:text-5xl font-bold leading-[1.2] tracking-tight', 2: 'text-xl md:text-3xl lg:text-4xl font-bold leading-[1.2] tracking-tight',
3: 'text-xl md:text-2xl lg:text-3xl font-bold leading-[1.3] 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]', 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]', 5: 'text-base md:text-lg font-bold leading-[1.5]',
6: 'text-base md:text-lg font-semibold leading-[1.6]', 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`); filePath = path.join(productsDir, `${fileSlug}-2.mdx`);
} }
let product: ProductMdx | null = null;
if (!fs.existsSync(filePath)) { if (!fs.existsSync(filePath)) {
// Fallback to English if locale is not 'en' // Fallback to English if locale is not 'en'
if (locale !== 'en') { if (locale !== 'en') {
@@ -43,7 +45,7 @@ export async function getProductBySlug(slug: string, locale: string): Promise<Pr
if (fs.existsSync(enFilePath)) { if (fs.existsSync(enFilePath)) {
const fileContent = fs.readFileSync(enFilePath, 'utf8'); const fileContent = fs.readFileSync(enFilePath, 'utf8');
const { data, content } = matter(fileContent); const { data, content } = matter(fileContent);
return { product = {
slug: fileSlug, slug: fileSlug,
frontmatter: { frontmatter: {
...data, ...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; return null;
} }
const fileContent = fs.readFileSync(filePath, 'utf8'); return product;
const { data, content } = matter(fileContent);
return {
slug: fileSlug,
frontmatter: data as ProductFrontmatter,
content,
};
} }
export async function getAllProductSlugs(locale: string): Promise<string[]> { 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", "lint": "next lint",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"test": "vitest run --passWithNoTests", "test": "vitest run --passWithNoTests",
"test:og": "vitest run tests/og-image.test.ts",
"pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts", "pdf:datasheets": "tsx ./scripts/generate-pdf-datasheets.ts",
"pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts" "pdf:datasheets:legacy": "tsx ./scripts/generate-pdf-datasheets-pdf-lib.ts"
}, },

View File

@@ -6,8 +6,6 @@ Sentry.init({
dsn, dsn,
enabled: Boolean(dsn), enabled: Boolean(dsn),
tracesSampleRate: 0, 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 // AdjustingreplaysOnErrorSampleRate to 1.0 to capture 100% of errors with replays if enabled
replaysOnErrorSampleRate: 1.0, replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.1, replaysSessionSampleRate: 0.1,

View File

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

View File

@@ -6,9 +6,5 @@ Sentry.init({
dsn, dsn,
enabled: Boolean(dsn), enabled: Boolean(dsn),
tracesSampleRate: 0, 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);
});