From d686b84c8684e1913215074ddad253aa6edcfa64 Mon Sep 17 00:00:00 2001 From: Hugo Gresse Date: Fri, 24 Apr 2026 08:22:20 +0200 Subject: [PATCH 1/2] Generate per-article OG images for blog posts Every blog article shared the default OG image because computeOgPath() mapped /blog/* to /og/default.png and the blog page layout never set an explicit metaImage. Shares from /blog/ on social platforms all looked identical. - Add src/pages/og/blog/[slug].png.ts that iterates the blog content collection and renders one PNG per article via pageTemplate, using the article title and locale-formatted date as copy. - Wire src/pages/blog/[...slug].astro to pass metaImage pointing at that endpoint. - Add a dedicated /og/pages/blog.png for the blog index (was still falling through to default). - Update computeOgPath: /blog now returns the blog index OG image; /blog/ articles pass an explicit metaImage from the page frontmatter so the fallback path is only used for unknown routes. Build regenerates 7 blog PNGs + 1 blog index PNG. Co-Authored-By: Claude Opus 4 (1M context) --- src/og/ogImage.ts | 4 +++ src/pages/blog/[...slug].astro | 6 +++- src/pages/og/blog/[slug].png.ts | 47 ++++++++++++++++++++++++++++++++ src/pages/og/pages/[page].png.ts | 5 ++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/pages/og/blog/[slug].png.ts diff --git a/src/og/ogImage.ts b/src/og/ogImage.ts index dff88a6..a8419fa 100644 --- a/src/og/ogImage.ts +++ b/src/og/ogImage.ts @@ -15,6 +15,10 @@ export function computeOgPath(pathname: string): string { if (clean === '/speakers') return '/og/pages/speakers.png' if (clean === '/' || clean === '') return '/og/default.png' if (clean.startsWith('/schedule')) return '/og/pages/schedule.png' + if (clean === '/blog') return '/og/pages/blog.png' + // Individual /blog/ articles pass an explicit metaImage (see + // src/pages/blog/[...slug].astro); a stray unknown /blog/* route falls + // back to default. if (clean.startsWith('/blog')) return '/og/default.png' const staticPages = ['team', 'jobs', 'location', 'anecdotes', 'jeu', 'coc', '404'] const seg = clean.replace(/^\//, '').split('/')[0] diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index 70b7863..647ff79 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -14,9 +14,13 @@ export async function getStaticPaths() { const { entry } = Astro.props const { Content } = await render(entry) +const ogImage = `${import.meta.env.SITE}/og/blog/${entry.id}.png` --- - + diff --git a/src/pages/og/blog/[slug].png.ts b/src/pages/og/blog/[slug].png.ts new file mode 100644 index 0000000..03b51bf --- /dev/null +++ b/src/pages/og/blog/[slug].png.ts @@ -0,0 +1,47 @@ +import type { APIRoute } from 'astro' +import { getCollection } from 'astro:content' +import { + getFlamingoDataUri, + getMonochromeLogoDataUri, + pngResponse, + readOgCache, + renderOg, + writeOgCache, +} from '../../../og/render' +import { pageTemplate } from '../../../og/templates' + +export async function getStaticPaths() { + const entries = await getCollection('blog') + return entries.map((entry) => ({ + params: { slug: entry.id }, + props: { + title: entry.data.title, + date: entry.data.date.toISOString(), + }, + })) +} + +export const GET: APIRoute = async ({ params, props }) => { + const cacheKey = `blog/${params.slug}.png` + const cached = await readOgCache(cacheKey) + if (cached) return pngResponse(cached) + + const [logo, flamingo] = await Promise.all([getMonochromeLogoDataUri(), getFlamingoDataUri()]) + const date = new Date(props.date).toLocaleDateString('fr-FR', { + day: 'numeric', + month: 'long', + year: 'numeric', + }) + const png = await renderOg( + pageTemplate( + logo, + { + eyebrow: `Blog · ${date}`, + title: props.title, + }, + flamingo + ) + ) + await writeOgCache(cacheKey, png) + return pngResponse(png) +} diff --git a/src/pages/og/pages/[page].png.ts b/src/pages/og/pages/[page].png.ts index 8ed03f6..b2e156f 100644 --- a/src/pages/og/pages/[page].png.ts +++ b/src/pages/og/pages/[page].png.ts @@ -55,6 +55,11 @@ const PAGES: Record = { title: 'Code de conduite', description: 'Une conférence respectueuse, inclusive et bienveillante pour tou·te·s.', }, + blog: { + eyebrow: 'Blog', + title: 'Blog', + description: "Coulisses, chiffres et décisions de l'équipe Sunny Tech.", + }, '404': { eyebrow: 'Erreur 404', title: 'Page introuvable', From 90b24e6bf166a1820167bef66cb3f5d8b32bdc13 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 24 Apr 2026 06:29:35 +0000 Subject: [PATCH 2/2] fix: version blog OG cache key with content hash to bust stale images Agent-Logs-Url: https://github.com/Sunny-Tech/SunnyTechWebsite/sessions/80f49c89-1d22-4189-a468-4fc3245403c1 Co-authored-by: HugoGresse <662377+HugoGresse@users.noreply.github.com> --- src/pages/og/blog/[slug].png.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/og/blog/[slug].png.ts b/src/pages/og/blog/[slug].png.ts index 03b51bf..7e23f29 100644 --- a/src/pages/og/blog/[slug].png.ts +++ b/src/pages/og/blog/[slug].png.ts @@ -1,3 +1,4 @@ +import { createHash } from 'node:crypto' import type { APIRoute } from 'astro' import { getCollection } from 'astro:content' import { @@ -22,7 +23,11 @@ export async function getStaticPaths() { } export const GET: APIRoute = async ({ params, props }) => { - const cacheKey = `blog/${params.slug}.png` + const contentHash = createHash('sha256') + .update(props.title + props.date) + .digest('hex') + .slice(0, 8) + const cacheKey = `blog/${params.slug}-${contentHash}.png` const cached = await readOgCache(cacheKey) if (cached) return pngResponse(cached)