From a2f94f15bc86ca83490aedc3439df09c7f3c5795 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Thu, 29 Jan 2026 17:26:02 +0100 Subject: [PATCH] og --- app/[locale]/[slug]/page.tsx | 2 + app/[locale]/api/og/product/route.tsx | 3 +- app/[locale]/blog/[slug]/page.tsx | 2 + app/[locale]/blog/page.tsx | 2 + app/[locale]/contact/page.tsx | 13 ++---- app/[locale]/opengraph-image.tsx | 2 +- app/[locale]/page.tsx | 4 +- app/[locale]/products/[...slug]/page.tsx | 19 ++------- app/[locale]/products/page.tsx | 2 + app/[locale]/team/page.tsx | 2 + components/OGImageTemplate.tsx | 2 + lib/metadata.ts | 24 +++++++++++ package.json | 1 + sentry.client.config.ts | 2 - sentry.edge.config.ts | 2 - sentry.server.config.ts | 4 -- tests/og-image.test.ts | 52 ++++++++++++++++++++++++ 17 files changed, 102 insertions(+), 36 deletions(-) create mode 100644 lib/metadata.ts create mode 100644 tests/og-image.test.ts diff --git a/app/[locale]/[slug]/page.tsx b/app/[locale]/[slug]/page.tsx index 23d0242e..547f4df7 100644 --- a/app/[locale]/[slug]/page.tsx +++ b/app/[locale]/[slug]/page.tsx @@ -5,6 +5,7 @@ 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', diff --git a/app/[locale]/api/og/product/route.tsx b/app/[locale]/api/og/product/route.tsx index 0c0c720a..d8d599a6 100644 --- a/app/[locale]/api/og/product/route.tsx +++ b/app/[locale]/api/og/product/route.tsx @@ -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( diff --git a/app/[locale]/blog/[slug]/page.tsx b/app/[locale]/blog/[slug]/page.tsx index dedadd25..c3f79c99 100644 --- a/app/[locale]/blog/[slug]/page.tsx +++ b/app/[locale]/blog/[slug]/page.tsx @@ -11,6 +11,7 @@ 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: { @@ -43,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', diff --git a/app/[locale]/blog/page.tsx b/app/[locale]/blog/page.tsx index 88bd2a3a..b4a456c0 100644 --- a/app/[locale]/blog/page.tsx +++ b/app/[locale]/blog/page.tsx @@ -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', diff --git a/app/[locale]/contact/page.tsx b/app/[locale]/contact/page.tsx index 4e68b022..56bf04db 100644 --- a/app/[locale]/contact/page.tsx +++ b/app/[locale]/contact/page.tsx @@ -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, diff --git a/app/[locale]/opengraph-image.tsx b/app/[locale]/opengraph-image.tsx index 8a405205..f67602ee 100644 --- a/app/[locale]/opengraph-image.tsx +++ b/app/[locale]/opengraph-image.tsx @@ -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' }); diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 8d452073..a9e46a9e 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -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', diff --git a/app/[locale]/products/[...slug]/page.tsx b/app/[locale]/products/[...slug]/page.tsx index 875221f0..d1ed843f 100644 --- a/app/[locale]/products/[...slug]/page.tsx +++ b/app/[locale]/products/[...slug]/page.tsx @@ -11,6 +11,7 @@ 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['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['images'] { + return [ + { + url: `${SITE_URL}/${locale}/api/og/product?slug=${slug}`, + width: 1200, + height: 630, + alt: title, + }, + ]; +} diff --git a/package.json b/package.json index ebd803af..24449db4 100644 --- a/package.json +++ b/package.json @@ -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" }, diff --git a/sentry.client.config.ts b/sentry.client.config.ts index 16d78afd..cd907476 100644 --- a/sentry.client.config.ts +++ b/sentry.client.config.ts @@ -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, diff --git a/sentry.edge.config.ts b/sentry.edge.config.ts index c87552cf..4beaf25f 100644 --- a/sentry.edge.config.ts +++ b/sentry.edge.config.ts @@ -6,7 +6,5 @@ Sentry.init({ dsn, enabled: Boolean(dsn), tracesSampleRate: 0, - // Ensure 500 errors are always captured - debug: process.env.NODE_ENV === 'development', }); diff --git a/sentry.server.config.ts b/sentry.server.config.ts index 2ca7ca5c..4beaf25f 100644 --- a/sentry.server.config.ts +++ b/sentry.server.config.ts @@ -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', }); diff --git a/tests/og-image.test.ts b/tests/og-image.test.ts new file mode 100644 index 00000000..378be568 --- /dev/null +++ b/tests/og-image.test.ts @@ -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); +});