From d36909b8b17f9859da025ad110a62a70c2c74212 Mon Sep 17 00:00:00 2001 From: Mao Nakamoto <41178744+maonakamoto@users.noreply.github.com> Date: Fri, 7 Aug 2026 10:53:00 +0200 Subject: [PATCH 1/2] feat(meta): give the site a real social preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This site shipped no og:image, so every link to it — Telegram, Slack, a DM — rendered as a blank grey rectangle. Adds the Next file-convention card (1200x630, the size those scrapers crop to) drawn in the site's own palette. Also sets metadataBase, which is load-bearing rather than cosmetic: without it Next resolves the generated image to http://localhost:3000/opengraph-image — a tag that is present, looks configured, and is unfetchable by every scraper. The fallback is the real host, so a missing env var degrades to correct rather than to localhost. site.url also pointed at botsmann.com, which has no DNS A record — so everything derived from it (canonical URL, og:image, sitemap) named a host that does not resolve. Corrected to where the site actually serves. Pattern verified end-to-end on a sibling repo: build, render, 200 image/png at 1200x630, and og:image emitted as the absolute production URL. --- app/layout.tsx | 11 +++++++ app/opengraph-image.tsx | 73 +++++++++++++++++++++++++++++++++++++++++ lib/site.ts | 6 +++- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 app/opengraph-image.tsx diff --git a/app/layout.tsx b/app/layout.tsx index 5f51af8e..801e80ab 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -24,8 +24,19 @@ const serif = Fraunces({ }); export const metadata = { + // Load-bearing: Next resolves the generated og:image against metadataBase. + // Without it the tag is emitted as http://localhost:3000/opengraph-image — + // present, plausible, and unfetchable by every social scraper. + metadataBase: new URL(site.url), title: `${site.name} — ${site.tagline}`, description: site.description, + openGraph: { + title: `${site.name} — ${site.tagline}`, + description: site.description, + url: site.url, + siteName: site.name, + type: 'website', + }, }; export default function RootLayout({ children }: { children: React.ReactNode }) { diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx new file mode 100644 index 00000000..73f5ace9 --- /dev/null +++ b/app/opengraph-image.tsx @@ -0,0 +1,73 @@ +import { ImageResponse } from 'next/og'; +import { site } from '@/lib/site'; + +/** + * Social preview card, generated by Next's file convention at 1200x630 — + * the 1.91:1 size Slack, Telegram and the OpenGraph scrapers crop to. + * + * Botsmann shipped no og:image, so every shared link rendered as a blank + * rectangle. Copy comes from lib/site.ts so the card cannot drift from the + * hero and footer; only the palette is repeated, because Satori cannot read + * CSS custom properties. globals.css stays the source of truth for those. + */ + +export const runtime = 'edge'; +export const alt = `${site.name} — ${site.tagline}`; +export const size = { width: 1200, height: 630 }; +export const contentType = 'image/png'; + +// Mirrors --primitive-ink-950 / --primitive-paper / --primitive-ochre-600 +const INK = '#1A1714'; +const PAPER = '#FAF8F4'; +const OCHRE = '#B45309'; +const MUTED = '#6F655C'; + +export default function OGImage() { + return new ImageResponse( + ( +