feat(og): add logo and dynamic faded background image to OG templates
Some checks failed
Build & Deploy / 🔍 Prepare (push) Successful in 11s
Build & Deploy / 🧪 QA (push) Successful in 1m31s
Build & Deploy / 🚀 Deploy (push) Has been cancelled
Build & Deploy / 🧪 Post-Deploy Verification (push) Has been cancelled
Build & Deploy / 🔔 Notify (push) Has been cancelled
Build & Deploy / 🏗️ Build (push) Has been cancelled

This commit is contained in:
2026-06-22 11:41:32 +02:00
parent bff150b1a7
commit 2c35c6b8aa
9 changed files with 101 additions and 25 deletions

View File

@@ -1,7 +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'; import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -20,9 +20,12 @@ export default async function Image({
} }
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate <OGImageTemplate
image={bgImage}
logo={logo}
title={pageData.frontmatter.title} title={pageData.frontmatter.title}
description={pageData.frontmatter.excerpt} description={pageData.frontmatter.excerpt}
label="Information" label="Information"

View File

@@ -1,7 +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'; import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
import { SITE_URL } from '@/lib/schema'; import { SITE_URL } from '@/lib/schema';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
@@ -35,6 +35,7 @@ export default async function Image({
} }
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
// We don't have request.url here, but we can assume the domain from SITE_URL or config // 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 // For local images during dev, relative paths in <img> might not work in Satori
@@ -55,10 +56,11 @@ export default async function Image({
return new ImageResponse( return new ImageResponse(
<OGImageTemplate <OGImageTemplate
logo={logo}
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={base64Image || featuredImage} image={base64Image || featuredImage || bgImage}
/>, />,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,

View File

@@ -1,7 +1,7 @@
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'; import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,9 +11,16 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Blog.meta' }); const t = await getTranslations({ locale, namespace: 'Blog.meta' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate title={t('title')} description={t('description')} label="Blog" />, <OGImageTemplate
image={bgImage}
logo={logo}
title={t('title')}
description={t('description')}
label="Blog"
/>,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,
fonts, fonts,

View File

@@ -1,7 +1,7 @@
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'; import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Contact' }); const t = await getTranslations({ locale, namespace: 'Contact' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
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');
return new ImageResponse( return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Contact" />, <OGImageTemplate
image={bgImage}
logo={logo}
title={title}
description={description}
label="Contact"
/>,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,
fonts, fonts,

View File

@@ -1,7 +1,7 @@
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'; import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,9 +11,12 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Index.meta' }); const t = await getTranslations({ locale, namespace: 'Index.meta' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
return new ImageResponse( return new ImageResponse(
<OGImageTemplate <OGImageTemplate
image={bgImage}
logo={logo}
title={t('title')} title={t('title')}
description={t('description')} description={t('description')}
label="Reliable Energy Infrastructure" label="Reliable Energy Infrastructure"

View File

@@ -1,7 +1,7 @@
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'; import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,6 +11,7 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Products' }); const t = await getTranslations({ locale, namespace: 'Products' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
const title = t.has('meta.title') const title = t.has('meta.title')
? t('meta.title') ? t('meta.title')
@@ -20,7 +21,13 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const description = t('meta.description') || t('subtitle'); const description = t('meta.description') || t('subtitle');
return new ImageResponse( return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Products" />, <OGImageTemplate
image={bgImage}
logo={logo}
title={title}
description={description}
label="Products"
/>,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,
fonts, fonts,

View File

@@ -1,7 +1,7 @@
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'; import { getOgFonts, OG_IMAGE_SIZE, getOgAssets } from '@/lib/og-helper';
export const size = OG_IMAGE_SIZE; export const size = OG_IMAGE_SIZE;
export const contentType = 'image/png'; export const contentType = 'image/png';
@@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri
const { locale } = await params; const { locale } = await params;
const t = await getTranslations({ locale, namespace: 'Team' }); const t = await getTranslations({ locale, namespace: 'Team' });
const fonts = await getOgFonts(); const fonts = await getOgFonts();
const { logo, bgImage } = getOgAssets();
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');
return new ImageResponse( return new ImageResponse(
<OGImageTemplate title={title} description={description} label="Our Team" />, <OGImageTemplate
image={bgImage}
logo={logo}
title={title}
description={description}
label="Our Team"
/>,
{ {
...OG_IMAGE_SIZE, ...OG_IMAGE_SIZE,
fonts, fonts,

View File

@@ -5,6 +5,7 @@ interface OGImageTemplateProps {
description?: string; description?: string;
label?: string; label?: string;
image?: string; image?: string;
logo?: string;
mode?: 'dark' | 'light' | 'image'; mode?: 'dark' | 'light' | 'image';
} }
@@ -13,6 +14,7 @@ export function OGImageTemplate({
description, description,
label, label,
image, image,
logo,
mode = 'dark', mode = 'dark',
}: OGImageTemplateProps) { }: OGImageTemplateProps) {
const primaryBlue = '#001a4d'; const primaryBlue = '#001a4d';
@@ -152,21 +154,26 @@ export function OGImageTemplate({
height: '6px', height: '6px',
backgroundColor: accentGreen, backgroundColor: accentGreen,
borderRadius: '3px', borderRadius: '3px',
marginRight: '24px', marginRight: '32px',
}} }}
/> />
<div {logo ? (
style={{ /* eslint-disable-next-line @next/next/no-img-element */
fontSize: '24px', <img src={logo} alt="KLZ Cables" height="40" style={{ objectFit: 'contain' }} />
fontWeight: 700, ) : (
color: 'white', <div
textTransform: 'uppercase', style={{
letterSpacing: '0.15em', fontSize: '24px',
display: 'flex', fontWeight: 700,
}} color: 'white',
> textTransform: 'uppercase',
KLZ Cables letterSpacing: '0.15em',
</div> display: 'flex',
}}
>
KLZ Cables
</div>
)}
</div> </div>
{/* Saturated Blue Brand Strip */} {/* Saturated Blue Brand Strip */}

View File

@@ -55,3 +55,36 @@ export const OG_IMAGE_SIZE = {
width: 1200, width: 1200,
height: 630, height: 630,
}; };
/**
* Loads a local image and converts it to a base64 data URI for Satori
*/
export function getLocalImageBase64(filename: string): string | undefined {
try {
const filePath = join(process.cwd(), 'public', filename);
const buffer = readFileSync(filePath);
const ext = filename.split('.').pop()?.toLowerCase();
const mimeType =
ext === 'png'
? 'image/png'
: ext === 'jpg' || ext === 'jpeg'
? 'image/jpeg'
: 'image/svg+xml';
return `data:${mimeType};base64,${buffer.toString('base64')}`;
} catch (error) {
console.error(`[OG] Failed to load local image ${filename}:`, error);
return undefined;
}
}
/**
* Returns standard assets for OG images
*/
export function getOgAssets() {
return {
logo: getLocalImageBase64('logo-white.png'),
bgImage: getLocalImageBase64(
'media/aerial-view-of-electricity-station-surrounded-with-2023-11-27-05-33-40-utc-scaled.jpg',
),
};
}