og
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m48s
All checks were successful
Build & Deploy KLZ Cables / build-and-deploy (push) Successful in 4m48s
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<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',
|
||||
|
||||
@@ -6,6 +6,7 @@ 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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -46,6 +46,8 @@ export function OGImageTemplate({
|
||||
<img
|
||||
src={image}
|
||||
alt=""
|
||||
width="1200"
|
||||
height="630"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
|
||||
24
lib/metadata.ts
Normal file
24
lib/metadata.ts
Normal 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,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6,7 +6,5 @@ Sentry.init({
|
||||
dsn,
|
||||
enabled: Boolean(dsn),
|
||||
tracesSampleRate: 0,
|
||||
// Ensure 500 errors are always captured
|
||||
debug: process.env.NODE_ENV === 'development',
|
||||
});
|
||||
|
||||
|
||||
@@ -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
52
tests/og-image.test.ts
Normal 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);
|
||||
});
|
||||
Reference in New Issue
Block a user