Generating Social Images

Hand-made share cards go stale the moment a title changes. Drop an opengraph-image.(jpg|jpeg|png|gif) or twitter-image.* file into a segment and Next.js 10,514 emits the tag family for it, with a sibling opengraph-image.alt.txt for the alt text; the build fails above 8 MB (5 MB for twitter-image). Give the file a .tsx extension and it becomes a route that renders the picture. ImageResponse from next/og turns JSX into a PNG — Satori 13,987 converts the tree to SVG, Resvg 4,096 rasterizes it — so only flexbox and a subset of CSS survive.

app/books/[slug]/opengraph-image.tsxShell
export const alt = "Book cover card", contentType = "image/png";
export const size = { width: 1200, height: 630 };
export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
  const book = await getBook((await params).slug);
  return new ImageResponse(
    <div style={{ width: "100%", height: "100%", display: "flex", padding: 72,
      flexDirection: "column", justifyContent: "space-between",
      background: "#0b1020", color: "#f8fafc", fontFamily: "sans-serif" }}>
      <div style={{ fontSize: 30, letterSpacing: 4, color: "#7dd3fc" }}>BOOKSHELF</div>
      <div style={{ fontSize: 76, lineHeight: 1.1 }}>{book?.title ?? "Unknown book"}</div>
      <div style={{ fontSize: 34, color: "#94a3b8" }}>
        {book && `${book.author} — ${book.rating}/5 from ${book.reviews} reviews`}
      </div>
    </div>, size,
  );
}

alt becomes og:image:alt, size — 1200x630 by default, which suits both networks — the width and height tags, contentType the og:image:type. The route returns a 40,837-byte image/png.

The card ImageResponse rendered for /books/the-pragmatic-programmer
The card ImageResponse rendered for /books/the-pragmatic-programmer

Generated images are statically optimized by default. This one is not, because it awaits an uncached lookup: next build lists ƒ /books/[slug]/opengraph-image as dynamic while the page is prerendered. Cache the loader and it joins the static column. A custom typeface is passed as bytes in the fonts option, read with readFile(join(process.cwd(), "Inter.ttf")) at module scope; only ttf, otf and woff are accepted.