From 2c35c6b8aaa3c21c46441d609a395a5f2e55b140 Mon Sep 17 00:00:00 2001 From: Marc Mintel Date: Mon, 22 Jun 2026 11:41:32 +0200 Subject: [PATCH] feat(og): add logo and dynamic faded background image to OG templates --- app/[locale]/[slug]/opengraph-image.tsx | 5 ++- app/[locale]/blog/[slug]/opengraph-image.tsx | 6 ++-- app/[locale]/blog/opengraph-image.tsx | 11 +++++-- app/[locale]/contact/opengraph-image.tsx | 11 +++++-- app/[locale]/opengraph-image.tsx | 5 ++- app/[locale]/products/opengraph-image.tsx | 11 +++++-- app/[locale]/team/opengraph-image.tsx | 11 +++++-- components/OGImageTemplate.tsx | 33 ++++++++++++-------- lib/og-helper.tsx | 33 ++++++++++++++++++++ 9 files changed, 101 insertions(+), 25 deletions(-) diff --git a/app/[locale]/[slug]/opengraph-image.tsx b/app/[locale]/[slug]/opengraph-image.tsx index 2fe4d8f9..fbcd92c9 100644 --- a/app/[locale]/[slug]/opengraph-image.tsx +++ b/app/[locale]/[slug]/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from 'next/og'; import { getPageBySlug } from '@/lib/pages'; 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 contentType = 'image/png'; @@ -20,9 +20,12 @@ export default async function Image({ } const fonts = await getOgFonts(); + const { logo, bgImage } = getOgAssets(); return new ImageResponse( might not work in Satori @@ -55,10 +56,11 @@ export default async function Image({ return new ImageResponse( , { ...OG_IMAGE_SIZE, diff --git a/app/[locale]/blog/opengraph-image.tsx b/app/[locale]/blog/opengraph-image.tsx index 3848a885..8e72ad61 100644 --- a/app/[locale]/blog/opengraph-image.tsx +++ b/app/[locale]/blog/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from 'next/og'; import { getTranslations } from 'next-intl/server'; 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 contentType = 'image/png'; @@ -11,9 +11,16 @@ export default async function Image({ params }: { params: Promise<{ locale: stri const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Blog.meta' }); const fonts = await getOgFonts(); + const { logo, bgImage } = getOgAssets(); return new ImageResponse( - , + , { ...OG_IMAGE_SIZE, fonts, diff --git a/app/[locale]/contact/opengraph-image.tsx b/app/[locale]/contact/opengraph-image.tsx index bd909b59..bd4011e4 100644 --- a/app/[locale]/contact/opengraph-image.tsx +++ b/app/[locale]/contact/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from 'next/og'; import { getTranslations } from 'next-intl/server'; 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 contentType = 'image/png'; @@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Contact' }); const fonts = await getOgFonts(); + const { logo, bgImage } = getOgAssets(); const title = t('meta.title') || t('title'); const description = t('meta.description') || t('subtitle'); return new ImageResponse( - , + , { ...OG_IMAGE_SIZE, fonts, diff --git a/app/[locale]/opengraph-image.tsx b/app/[locale]/opengraph-image.tsx index c28014fe..05b02b9a 100644 --- a/app/[locale]/opengraph-image.tsx +++ b/app/[locale]/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from 'next/og'; import { getTranslations } from 'next-intl/server'; 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 contentType = 'image/png'; @@ -11,9 +11,12 @@ export default async function Image({ params }: { params: Promise<{ locale: stri const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Index.meta' }); const fonts = await getOgFonts(); + const { logo, bgImage } = getOgAssets(); return new ImageResponse( , + , { ...OG_IMAGE_SIZE, fonts, diff --git a/app/[locale]/team/opengraph-image.tsx b/app/[locale]/team/opengraph-image.tsx index a08a2388..d28961b4 100644 --- a/app/[locale]/team/opengraph-image.tsx +++ b/app/[locale]/team/opengraph-image.tsx @@ -1,7 +1,7 @@ import { ImageResponse } from 'next/og'; import { getTranslations } from 'next-intl/server'; 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 contentType = 'image/png'; @@ -11,12 +11,19 @@ export default async function Image({ params }: { params: Promise<{ locale: stri const { locale } = await params; const t = await getTranslations({ locale, namespace: 'Team' }); const fonts = await getOgFonts(); + const { logo, bgImage } = getOgAssets(); const title = t('meta.title') || t('hero.subtitle'); const description = t('meta.description') || t('hero.title'); return new ImageResponse( - , + , { ...OG_IMAGE_SIZE, fonts, diff --git a/components/OGImageTemplate.tsx b/components/OGImageTemplate.tsx index 16bc93c8..fd3afc5a 100644 --- a/components/OGImageTemplate.tsx +++ b/components/OGImageTemplate.tsx @@ -5,6 +5,7 @@ interface OGImageTemplateProps { description?: string; label?: string; image?: string; + logo?: string; mode?: 'dark' | 'light' | 'image'; } @@ -13,6 +14,7 @@ export function OGImageTemplate({ description, label, image, + logo, mode = 'dark', }: OGImageTemplateProps) { const primaryBlue = '#001a4d'; @@ -152,21 +154,26 @@ export function OGImageTemplate({ height: '6px', backgroundColor: accentGreen, borderRadius: '3px', - marginRight: '24px', + marginRight: '32px', }} /> -
- KLZ Cables -
+ {logo ? ( + /* eslint-disable-next-line @next/next/no-img-element */ + KLZ Cables + ) : ( +
+ KLZ Cables +
+ )} {/* Saturated Blue Brand Strip */} diff --git a/lib/og-helper.tsx b/lib/og-helper.tsx index 4f36be5e..8ee4b474 100644 --- a/lib/og-helper.tsx +++ b/lib/og-helper.tsx @@ -55,3 +55,36 @@ export const OG_IMAGE_SIZE = { width: 1200, 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', + ), + }; +}