The og:image That Disappeared on Every Interior Page
We shipped the 21. mark as a link-preview image for the site. The intent: every time someone drops the URL into iMessage, Slack, or LinkedIn, the 21. mark fills the card instead of the site defaulting to the largest image on the page.
The og:image tag showed up immediately on the homepage. It was missing on every other route.
Why it breaks
Next.js supports two ways to set metadata. The file convention places a file named opengraph-image.png in a route segment directory. The export convention uses export const metadata or generateMetadata in page.tsx. Both are legitimate. The problem is what happens when you mix them.
The file at src/app/opengraph-image.png sits in the root app directory. Per the docs, Next picks it up automatically and attaches it to the root layout. Every page inherits it, right?
Not if that page exports its own metadata object with an openGraph key.
Next.js does not deep-merge metadata. A page-level openGraph object replaces the parent's openGraph object entirely. Once any page exports { openGraph: { title: "..." } }, the image from the root file convention is gone for that route. It doesn't carry forward. You get the og:title you set and nothing else.
The result: every interior page with its own metadata had no og:image. The homepage had none of its own metadata configuration, so it fell through to the root file. Every other page overwrote it.
We verified this with curl against production before touching anything:
curl -s https://twenty1-media.com/about | grep og:image
Empty. Same on /services, /blog, /contact, and every work and blog slug page.
The fix
The documented solution is straightforward once you understand the behavior: attach the image to every openGraph object explicitly. We exported a single ogImage variable from src/content/seo.ts and added it to every metadata block in the codebase.
export const ogImage = {
url: "/opengraph-image.png",
width: 1200,
height: 630,
alt: "Twenty1 Media",
};
Then in each page.tsx or generateMetadata call:
import { ogImage } from "@/content/seo";
export const metadata: Metadata = {
openGraph: {
title: "Page Title",
description: "...",
images: [ogImage],
},
};
Seven files touched: the root layout, the blog slug route, the work slug route, the locations route, the glossary, and two top-level page files. Each one now re-attaches the image explicitly rather than relying on inheritance that doesn't exist.
The verification step that matters
The gotcha with og:image is that next build succeeds whether or not the tag is present. Tests pass. The page renders. There is no warning. The only way to catch it is to check the rendered HTML on a production build or a next start output.
After the fix we ran:
curl -s https://twenty1-media.com/about | grep og:image
All seven routes returned the absolute image URL. The image itself served 200 from the optimizer. That is the check worth running, not just a local build.
The pattern to watch for
Any time you add a new route with its own openGraph metadata, the image will be missing unless you include images: [ogImage] in that block. We made ogImage a named export specifically to make this mechanical: grep for openGraph: and confirm images is present. One missing property, one missing import, and the card falls back to whatever the platform scrapes from the page body.
For social-first pages, that fallback is whatever image happens to be largest on the page. On our /about route, that was the founder photo. Not wrong, but not intentional either.