feat: Centralize OG image font loading and sizing, simplify product page OG generation, and refine template styling.
Some checks failed
Build & Deploy KLZ Cables / 🔍 Prepare Environment (push) Successful in 21s
Build & Deploy KLZ Cables / 🧪 Quality Assurance (push) Successful in 1m36s
Build & Deploy KLZ Cables / 🏗️ Build & Push (push) Failing after 1m31s
Build & Deploy KLZ Cables / 🚀 Deploy (push) Has been skipped
Build & Deploy KLZ Cables / 🔔 Notifications (push) Successful in 3s

This commit is contained in:
2026-02-01 11:05:37 +01:00
parent 5f9ee7d976
commit 03e597442b
15 changed files with 3074 additions and 144 deletions

View File

@@ -1,6 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getPageBySlug } from '@/lib/pages'; import { getPageBySlug } from '@/lib/pages';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
@@ -8,11 +9,11 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
const pageData = await getPageBySlug(slug, locale); const pageData = await getPageBySlug(slug, locale);
if (!pageData) { if (!pageData) {
return new ImageResponse( return new Response('Page not found', { status: 404 });
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
} }
const fonts = await getOgFonts();
return new ImageResponse( return new ImageResponse(
( (
<OGImageTemplate <OGImageTemplate
@@ -22,8 +23,9 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -3,6 +3,7 @@ import { getProductBySlug } from '@/lib/mdx';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { NextRequest } from 'next/server'; import { NextRequest } from 'next/server';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
@@ -10,7 +11,7 @@ export async function GET(
request: NextRequest, request: NextRequest,
{ params }: { params: { locale: string } } { params }: { params: { locale: string } }
) { ) {
const { searchParams } = new URL(request.url); const { searchParams, origin } = new URL(request.url);
const slug = searchParams.get('slug'); const slug = searchParams.get('slug');
const locale = params.locale || 'en'; const locale = params.locale || 'en';
@@ -18,6 +19,7 @@ export async function GET(
return new Response('Missing slug', { status: 400 }); return new Response('Missing slug', { status: 400 });
} }
const fonts = await getOgFonts();
const t = await getTranslations({ locale, namespace: 'Products' }); const t = await getTranslations({ locale, namespace: 'Products' });
// Check if it's a category page // Check if it's a category page
@@ -36,8 +38,8 @@ export async function GET(
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }
@@ -45,16 +47,13 @@ export async function GET(
const product = await getProductBySlug(slug, locale); const product = await getProductBySlug(slug, locale);
if (!product) { if (!product) {
return new ImageResponse( return new Response('Product not found', { status: 404 });
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
} }
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]
: `${origin}${product.frontmatter.images[0]}`) : `${origin}${product.frontmatter.images[0]}`)
: undefined; : undefined;
return new ImageResponse( return new ImageResponse(
@@ -63,12 +62,13 @@ export async function GET(
title={product.frontmatter.title} title={product.frontmatter.title}
description={product.frontmatter.description} description={product.frontmatter.description}
label={product.frontmatter.categories?.[0] || 'Product'} label={product.frontmatter.categories?.[0] || 'Product'}
image={featuredImage?.startsWith('http') ? featuredImage : undefined} image={featuredImage}
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -1,6 +1,7 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getPostBySlug } from '@/lib/blog'; import { getPostBySlug } from '@/lib/blog';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
@@ -8,15 +9,19 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
const post = await getPostBySlug(slug, locale); const post = await getPostBySlug(slug, locale);
if (!post) { if (!post) {
return new ImageResponse( return new Response('Post not found', { status: 404 });
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
} }
const fonts = await getOgFonts();
// We don't have request.url here, but we can assume the domain from SITE_URL or config
// For local images during dev, relative paths in <img> might not work in Satori
// but if we are in nodejs runtime, we could potentially read from disk.
// For now, let's just make sure it's absolute.
const featuredImage = post.frontmatter.featuredImage const featuredImage = post.frontmatter.featuredImage
? (post.frontmatter.featuredImage.startsWith('http') ? (post.frontmatter.featuredImage.startsWith('http')
? post.frontmatter.featuredImage ? post.frontmatter.featuredImage
: `https://klz-cables.com${post.frontmatter.featuredImage}`) : `https://klz-cables.com${post.frontmatter.featuredImage}`)
: undefined; : undefined;
return new ImageResponse( return new ImageResponse(
@@ -25,12 +30,13 @@ export default async function Image({ params: { locale, slug } }: { params: { lo
title={post.frontmatter.title} title={post.frontmatter.title}
description={post.frontmatter.excerpt} description={post.frontmatter.excerpt}
label={post.frontmatter.category || 'Blog'} label={post.frontmatter.category || 'Blog'}
image={featuredImage?.startsWith('http') ? featuredImage : undefined} image={featuredImage}
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -1,25 +1,25 @@
import { ImageResponse } from 'next/og'; 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';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; 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: 'Blog.meta' }); const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const title = t('title'); const fonts = await getOgFonts();
const description = t('description');
return new ImageResponse( return new ImageResponse(
( (
<OGImageTemplate <OGImageTemplate
title={title} title={t('title')}
description={description} description={t('description')}
label="Blog" label="Blog"
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -1,11 +1,14 @@
import { ImageResponse } from 'next/og'; 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';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; 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: 'Contact' }); const t = await getTranslations({ locale, namespace: 'Contact' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('title'); const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle'); const description = t('meta.description') || t('subtitle');
@@ -18,8 +21,8 @@ export default async function Image({ params: { locale } }: { params: { locale:
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -1,11 +1,13 @@
import { ImageResponse } from 'next/og'; 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';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; 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' });
const fonts = await getOgFonts();
return new ImageResponse( return new ImageResponse(
( (
@@ -16,8 +18,9 @@ export default async function Image({ params: { locale } }: { params: { locale:
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -1,83 +1,29 @@
import { ImageResponse } from 'next/og'; import { ImageResponse } from 'next/og';
import { getProductBySlug } from '@/lib/mdx';
import { getTranslations } from 'next-intl/server'; import { getTranslations } from 'next-intl/server';
import { OGImageTemplate } from '@/components/OGImageTemplate'; import { OGImageTemplate } from '@/components/OGImageTemplate';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; export const runtime = 'nodejs';
export default async function Image({ params: { locale, slug } }: { params: { locale: string, slug?: string[] } }) { export default async function Image({ params: { locale } }: { params: { locale: string } }) {
const t = await getTranslations('Products'); const t = await getTranslations({ locale, namespace: 'Products' });
const fonts = await getOgFonts();
// If no slug, it's the main products page const title = t('meta.title') || t('title');
if (!slug || slug.length === 0) { const description = t('meta.description') || t('subtitle');
const title = t('meta.title') || t('title');
const description = t('meta.description') || t('subtitle');
return new ImageResponse(
(
<OGImageTemplate
title={title}
description={description}
label="Products"
/>
),
{
width: 1200,
height: 630,
}
);
}
const productSlug = slug[slug.length - 1];
// Check if it's a category page
const categories = ['low-voltage-cables', 'medium-voltage-cables', 'high-voltage-cables', 'solar-cables'];
if (categories.includes(productSlug)) {
const categoryKey = productSlug.replace(/-cables$/, '').replace(/-([a-z])/g, (g) => g[1].toUpperCase());
const categoryTitle = t.has(`categories.${categoryKey}.title`) ? t(`categories.${categoryKey}.title`) : productSlug;
const categoryDesc = t.has(`categories.${categoryKey}.description`) ? t(`categories.${categoryKey}.description`) : '';
return new ImageResponse(
(
<OGImageTemplate
title={categoryTitle}
description={categoryDesc}
label="Product Category"
/>
),
{
width: 1200,
height: 630,
}
);
}
const product = await getProductBySlug(productSlug, locale);
if (!product) {
return new ImageResponse(
<div style={{ display: 'flex', width: '100%', height: '100%', backgroundColor: '#001a4d' }} />
);
}
const featuredImage = product.frontmatter.images?.[0]
? (product.frontmatter.images[0].startsWith('http')
? product.frontmatter.images[0]
: `https://klz-cables.com${product.frontmatter.images[0]}`)
: undefined;
return new ImageResponse( return new ImageResponse(
( (
<OGImageTemplate <OGImageTemplate
title={product.frontmatter.title} title={title}
description={product.frontmatter.description} description={description}
label={product.frontmatter.categories?.[0] || 'Product'} label="Products"
image={featuredImage?.startsWith('http') ? featuredImage : undefined}
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -1,11 +1,14 @@
import { ImageResponse } from 'next/og'; 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';
import { getOgFonts, OG_IMAGE_SIZE } from '@/lib/og-helper';
export const runtime = 'nodejs'; 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: 'Team' }); const t = await getTranslations({ locale, namespace: 'Team' });
const fonts = await getOgFonts();
const title = t('meta.title') || t('hero.subtitle'); const title = t('meta.title') || t('hero.subtitle');
const description = t('meta.description') || t('hero.title'); const description = t('meta.description') || t('hero.title');
@@ -18,8 +21,9 @@ export default async function Image({ params: { locale } }: { params: { locale:
/> />
), ),
{ {
width: 1200, ...OG_IMAGE_SIZE,
height: 630, fonts,
} }
); );
} }

View File

@@ -29,6 +29,7 @@ export function OGImageTemplate({
backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue, backgroundColor: mode === 'light' ? '#ffffff' : primaryBlue,
padding: '80px', padding: '80px',
position: 'relative', position: 'relative',
fontFamily: 'Inter',
}; };
return ( return (
@@ -63,22 +64,22 @@ export function OGImageTemplate({
left: 0, left: 0,
right: 0, right: 0,
bottom: 0, bottom: 0,
background: 'linear-gradient(to right, rgba(0,26,77,0.9), rgba(0,26,77,0.4))', background: 'linear-gradient(to right, rgba(0,26,77,0.95), rgba(0,26,77,0.6))',
}} }}
/> />
</div> </div>
)} )}
{/* Decorative Scribble Circle (Simplified for Satori) */} {/* Decorative Brand Accent (Top Right) */}
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: '-100px', top: '-150px',
right: '-100px', right: '-150px',
width: '600px', width: '600px',
height: '600px', height: '600px',
borderRadius: '300px', borderRadius: '300px',
backgroundColor: `${accentGreen}1a`, backgroundColor: `${accentGreen}15`,
display: 'flex', display: 'flex',
}} }}
/> />
@@ -89,11 +90,11 @@ export function OGImageTemplate({
<div <div
style={{ style={{
fontSize: '24px', fontSize: '24px',
fontWeight: 'bold', fontWeight: 700,
color: accentGreen, color: accentGreen,
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '0.2em', letterSpacing: '0.3em',
marginBottom: '24px', marginBottom: '32px',
display: 'flex', display: 'flex',
}} }}
> >
@@ -104,13 +105,14 @@ export function OGImageTemplate({
{/* Title */} {/* Title */}
<div <div
style={{ style={{
fontSize: '72px', fontSize: title.length > 40 ? '64px' : '82px',
fontWeight: '900', fontWeight: 700,
color: 'white', color: 'white',
lineHeight: '1.1', lineHeight: '1.05',
maxWidth: '900px', maxWidth: '950px',
marginBottom: '32px', marginBottom: '40px',
display: 'flex', display: 'flex',
letterSpacing: '-0.02em',
}} }}
> >
{title} {title}
@@ -121,13 +123,14 @@ export function OGImageTemplate({
<div <div
style={{ style={{
fontSize: '32px', fontSize: '32px',
color: 'rgba(255,255,255,0.8)', color: 'rgba(255,255,255,0.7)',
maxWidth: '800px', maxWidth: '850px',
lineHeight: '1.4', lineHeight: '1.4',
display: 'flex', display: 'flex',
fontWeight: 400,
}} }}
> >
{description} {description.length > 160 ? description.substring(0, 157) + '...' : description}
</div> </div>
)} )}
</div> </div>
@@ -144,33 +147,34 @@ export function OGImageTemplate({
> >
<div <div
style={{ style={{
width: '120px', width: '80px',
height: '8px', height: '6px',
backgroundColor: accentGreen, backgroundColor: accentGreen,
borderRadius: '4px', borderRadius: '3px',
marginRight: '24px', marginRight: '24px',
}} }}
/> />
<div <div
style={{ style={{
fontSize: '24px', fontSize: '24px',
fontWeight: 'bold', fontWeight: 700,
color: 'white', color: 'white',
textTransform: 'uppercase', textTransform: 'uppercase',
letterSpacing: '0.1em', letterSpacing: '0.15em',
display: 'flex',
}} }}
> >
KLZ Cables KLZ Cables
</div> </div>
</div> </div>
{/* Saturated Blue Accent */} {/* Saturated Blue Brand Strip */}
<div <div
style={{ style={{
position: 'absolute', position: 'absolute',
top: 0, top: 0,
right: 0, right: 0,
width: '10px', width: '12px',
height: '100%', height: '100%',
backgroundColor: saturatedBlue, backgroundColor: saturatedBlue,
}} }}
@@ -178,3 +182,4 @@ export function OGImageTemplate({
</div> </div>
); );
} }

42
lib/og-helper.tsx Normal file
View File

@@ -0,0 +1,42 @@
import { readFileSync } from 'fs';
import { join } from 'path';
/**
* Loads the Inter fonts for use in Satori (Next.js OG Image generation).
* Since we are using runtime = 'nodejs', we can read them from the filesystem.
*/
export async function getOgFonts() {
const boldFontPath = join(process.cwd(), 'public/fonts/Inter-Bold.ttf');
const regularFontPath = join(process.cwd(), 'public/fonts/Inter-Regular.ttf');
try {
const boldFont = readFileSync(boldFontPath);
const regularFont = readFileSync(regularFontPath);
return [
{
name: 'Inter',
data: boldFont,
weight: 700 as const,
style: 'normal' as const,
},
{
name: 'Inter',
data: regularFont,
weight: 400 as const,
style: 'normal' as const,
},
];
} catch (error) {
console.error('Failed to load OG fonts from filesystem, falling back to system fonts:', error);
return [];
}
}
/**
* Common configuration for OG images
*/
export const OG_IMAGE_SIZE = {
width: 1200,
height: 630,
};

View File

@@ -305,7 +305,6 @@ const getLabels = (locale: 'en' | 'de') => {
export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
product, product,
locale, locale,
logoUrl = '/media/logo.svg',
}) => { }) => {
const labels = getLabels(locale); const labels = getLabels(locale);
@@ -338,8 +337,12 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
</View> </View>
<View style={styles.productImageCol}> <View style={styles.productImageCol}>
{product.featuredImage ? ( {product.featuredImage ? (
<Image src={product.featuredImage} style={styles.heroImage} /> <Image
src={product.featuredImage}
style={styles.heroImage}
/>
) : ( ) : (
<Text style={styles.noImage}>{labels.noImage}</Text> <Text style={styles.noImage}>{labels.noImage}</Text>
)} )}
</View> </View>
@@ -370,7 +373,7 @@ export const PDFDatasheet: React.FC<PDFDatasheetProps> = ({
style={[ style={[
styles.specsTableRow, styles.specsTableRow,
index === product.attributes.length - 1 && index === product.attributes.length - 1 &&
styles.specsTableRowLast, styles.specsTableRowLast,
]} ]}
> >
<View style={styles.specsTableLabelCell}> <View style={styles.specsTableLabelCell}>

1447
public/fonts/Inter-Bold.ttf Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -1,52 +1,74 @@
import { describe, it, expect } from 'vitest'; import { describe, it, expect, beforeAll } from 'vitest';
const BASE_URL = process.env.TEST_URL || 'http://localhost:3000'; const BASE_URL = process.env.TEST_URL || process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
describe('OG Image Generation', () => { describe('OG Image Generation', () => {
const locales = ['de', 'en']; const locales = ['de', 'en'];
const productSlugs = ['nay2y']; // Based on data/products/de/nay2y.mdx const productSlugs = ['nay2y'];
let isServerUp = false;
beforeAll(async () => {
try {
const response = await fetch(`${BASE_URL}/health`).catch(() => null);
if (response && response.ok) {
const text = await response.text();
if (text.includes('OK')) {
isServerUp = true;
return;
}
}
console.log(`\n⚠ KLZ Application not detected at ${BASE_URL}. Skipping integration tests.\n`);
} catch (e) {
isServerUp = false;
}
});
async function verifyImageResponse(response: Response) { async function verifyImageResponse(response: Response) {
expect(response.status).toBe(200); expect(response.status, `Failed to fetch OG image: ${response.url}`).toBe(200);
expect(response.headers.get('content-type')).toContain('image/png'); const contentType = response.headers.get('content-type');
expect(contentType, `Incorrect content type: ${contentType}`).toContain('image/png');
const buffer = await response.arrayBuffer(); const buffer = await response.arrayBuffer();
const bytes = new Uint8Array(buffer); const bytes = new Uint8Array(buffer);
// Check for PNG signature: 89 50 4E 47 0D 0A 1A 0A // Check for PNG signature: 89 50 4E 47 0D 0A 1A 0A
expect(bytes[0]).toBe(0x89); expect(bytes[0]).toBe(0x89);
expect(bytes[1]).toBe(0x50); expect(bytes[1]).toBe(0x50);
expect(bytes[2]).toBe(0x4E); expect(bytes[2]).toBe(0x4E);
expect(bytes[3]).toBe(0x47); expect(bytes[3]).toBe(0x47);
// Check that the image is not empty and has a reasonable size // Check that the image is not empty and has a reasonable size
// A 1200x630 OG image should be at least 4KB expect(bytes.length, `Image size too small: ${bytes.length} bytes`).toBeGreaterThan(4000);
expect(bytes.length).toBeGreaterThan(4000);
} }
locales.forEach((locale) => { locales.forEach((locale) => {
it(`should generate main OG image for ${locale}`, async () => { it(`should generate main OG image for ${locale}`, async ({ skip }) => {
if (!isServerUp) skip();
const url = `${BASE_URL}/${locale}/opengraph-image`; const url = `${BASE_URL}/${locale}/opengraph-image`;
const response = await fetch(url); const response = await fetch(url);
await verifyImageResponse(response); await verifyImageResponse(response);
}, 30000); }, 30000);
it(`should generate product OG image for ${locale} with slug ${productSlugs[0]}`, async () => { it(`should generate product OG image for ${locale} with slug ${productSlugs[0]}`, async ({ skip }) => {
if (!isServerUp) skip();
const url = `${BASE_URL}/${locale}/api/og/product?slug=${productSlugs[0]}`; const url = `${BASE_URL}/${locale}/api/og/product?slug=${productSlugs[0]}`;
const response = await fetch(url); const response = await fetch(url);
await verifyImageResponse(response); await verifyImageResponse(response);
}, 30000); }, 30000);
it(`should return 400 for product OG image without slug in ${locale}`, async () => { it(`should return 400 for product OG image without slug in ${locale}`, async ({ skip }) => {
if (!isServerUp) skip();
const url = `${BASE_URL}/${locale}/api/og/product`; const url = `${BASE_URL}/${locale}/api/og/product`;
const response = await fetch(url); const response = await fetch(url);
expect(response.status).toBe(400); expect(response.status).toBe(400);
}, 30000); }, 30000);
}); });
it('should generate blog OG image', async () => { it('should generate blog OG image', async ({ skip }) => {
if (!isServerUp) skip();
const url = `${BASE_URL}/de/blog/opengraph-image`; const url = `${BASE_URL}/de/blog/opengraph-image`;
const response = await fetch(url); const response = await fetch(url);
await verifyImageResponse(response); await verifyImageResponse(response);
}, 30000); }, 30000);
}); });

File diff suppressed because one or more lines are too long