Blog Generate dynamic Open Graph images in Next.js
Generate dynamic Open Graph images in Next.js
TL;DR Add an opengraph-image.tsx file to a route in the Next.js App Router. It renders JSX to a PNG with next/og's ImageResponse, and Next wires it into the page's og:image and twitter:image automatically. Add generateStaticParams to prerender one per dynamic page.
A good social card lifts click-through when your links get shared, but hand-designing one per page does not scale. In the Next.js App Router you can generate a branded image for every page from its title, at build time, with no design tool. Here is the whole setup.
The file convention
Drop an opengraph-image.tsx file next to a route's page.tsx. Next treats it as the source of that route's og:image and twitter:image. You do not wire anything up in your metadata; Next does it for you.
A minimal image
The file default-exports a function that returns an ImageResponse. You build the picture with JSX and inline styles.
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default function Image() {
return new ImageResponse(
(
<div
style={{
height: "100%",
width: "100%",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#0a0e17",
color: "#fff",
fontSize: 64,
}}
>
My Site
</div>
),
size,
);
}
That already gives every page a branded 1200 by 630 card.
One image per dynamic page
For a blog with a [slug] route, you want each post's image to show its own title. Read the slug, look up the post, and render the title. Add generateStaticParams so Next prerenders one image per post at build time instead of on every request.
export function generateStaticParams() {
return getAllSlugs().map((slug) => ({ slug }));
}
export default async function Image({ params }) {
const { slug } = await params;
const post = getPost(slug);
// ...render post.title into the ImageResponse
}
Gotchas worth knowing
- Edge runtime and generateStaticParams do not mix. If you prerender with
generateStaticParams, do not also setexport const runtime = "edge". Let it run in the default runtime and the images become static files. - Inline styles only, and flexbox. The renderer supports a subset of CSS. Use
display: flexon containers with multiple children, and keep styling inline. - Fonts are not automatic. The default font works out of the box. For a custom font, load the file and pass it to
ImageResponse.
Why it is worth it
Every shared link now carries a clean, on-brand image with the page's own title, generated from one template you maintain in a few lines. On this site the same image doubles as the post's on-page hero, so one file feeds both the social card and the page. That is a lot of polish for very little code.
FAQ
Do I need a design tool for social images?
No. You describe the image as JSX with inline styles, and next/og renders it to a PNG. You get a consistent, branded card for every page from a single template, with no manual design per post.
Are the images generated on every request?
They can be, but for content known at build time you should prerender them. Add generateStaticParams so each page's image is generated once during the build and served as a static file, which is fast and cheap.
Can I use custom fonts?
Yes, but you must load the font data and pass it to ImageResponse, because the renderer does not have access to the browser's fonts. For a first version, the default sans-serif works and keeps the setup simple.