Lesson 45 of 81
The metadata lesson wires up the <meta> tags that point at a share image — this is the file convention that renders the image itself, on demand, as a PNG built from JSX.
12 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
Every social platform that unfurls a link reads an og:image tag and fetches whatever URL it points at. The static way to satisfy that is a designer-made PNG dropped in public/. The dynamic way — Next.js's opengraph-image file convention — is a special route file that generates that PNG on request, using ImageResponse, which takes JSX and CSS-in-JS-style inline styles and rasterizes them into an image via a WASM-based renderer (Satori) rather than a real browser.
Drop opengraph-image.tsx next to any page.tsx, export a default function returning an ImageResponse, and Next.js does two things automatically: it renders the route at build or request time to produce the actual PNG, and it injects the correct <meta property="og:image"> (and the matching Twitter card tag) into that segment's metadata — no manual openGraph.images array required. A sibling twitter-image.tsx does the same for a card that should differ from the OG one; omit it and Next.js reuses the OG image for both.
// app/learn/[track]/[lesson]/opengraph-image.tsx
import { ImageResponse } from "next/og";
import { getLesson } from "@/lib/content/lessons";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image({
params,
}: {
params: Promise<{ track: string; lesson: string }>;
}) {
const { track, lesson } = await params;
const data = getLesson(track, lesson);
const title = data?.frontmatter.title ?? "Praxis";
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "center",
padding: "80px 96px",
background: "#0a0b0e",
color: "#f7f7f8",
fontSize: 60,
fontWeight: 800,
}}
>
<div style={{ fontSize: 28, color: "#00d89d", display: "flex" }}>{track.toUpperCase()}</div>
<div style={{ marginTop: 24, display: "flex" }}>{title}</div>
</div>
),
{ ...size }
);
}
This file lives at exactly the same route segment as the lesson's page.tsx, so it inherits that segment's params the same way generateMetadata does — the two functions aren't linked to each other, but they're both resolving against the same URL, which is why this file can independently fetch the same lesson data and stay in sync with the page's own title. Every top-level <div> in the JSX tree needs an explicit display: "flex" (or another explicit value) — Satori doesn't apply CSS's normal default display: block, and a <div> with no display style silently renders as if it weren't there.
ImageResponse isn't screenshotting a rendered page. There's no headless browser involved — Satori walks the JSX tree directly and rasterizes it, which is why only a subset of CSS works and why layout bugs here don't necessarily match how the same JSX would render as real HTML.openGraph.images pointing at the same route is redundant, and can conflict. Adding opengraph-image.tsx is the entire integration; there's no separate step of also setting metadata.openGraph.images to reference it.opengraph-image.tsx that reads only static data can be generated once at build time; one that reads params to fetch per-request content (as in the lesson example) is generated dynamically, with the same caching tradeoffs as the page it sits beside.display on a flex child is the single most common "why is my layout blank/broken" bug — Satori's default differs from a real browser's, and there's no warning when a <div> is silently collapsed to nothing.fonts option — you can't rely on a font-family CSS string resolving the way it would in a browser, because there's no system font fallback chain to fall back to.The metadata API lesson covers how openGraph fields get merged and resolved into <head> tags across nested layouts — this is the sibling piece that generates the actual image bytes those tags point at, using the same per-segment params a generateMetadata function in the same route would receive.