Build Dynamic Open Graph (OG) Images
Create unique images for each blog post dynamically with Next.js
When creating content for the web, ensuring that it looks great when shared across social media platforms is essential. play a crucial role in defining how your webpage appears when shared on platforms like Facebook, Twitter, or LinkedIn. These tags provide metadata about your webpage, including its title, description, and image, influencing how users perceive and engage with your content.
images are a powerful part of this metadata. Specified using the og:image meta tag, they allow you to customize the visual content displayed alongside links to your site. By setting up dynamic OG images, you can make your blog posts more visually appealing, relevant, and engaging for your audience.
This guide will show you how to dynamically generate OG images for your blog using , enabling each post to have a unique, shareable image that stands out. The example is the actual implementation this blog runs in production, so everything below is lifted from working code.
Step 1: Install the Dependency
First, ensure you have a Next.js project set up. The image generation itself is done by under the hood, which you get through one of two packages:
Modern versions of Next.js (13.3 and later) also ship the same functionality built in as next/og, so if you are on a recent version you can skip the install entirely and import ImageResponse from next/og instead. This blog uses the standalone @vercel/og package; the API is identical.
ImageResponse lets you describe an image with JSX and a subset of CSS (flexbox via Tailwind-style tw props or inline styles), and it renders the result to a PNG on the server. That is all we need to build a card per post.
Step 2: Build an API Route for OG Images
Next, create a dynamic API route in your Next.js project to serve OG images. Since I am using the app router, the route lives in app/api/og/route.js.
I have a setup in my project, which gives me all the blog posts as a generated allPosts collection. The route takes a slug query parameter, finds the matching post, and falls back to the site title and description when there is no match (which is how the homepage and category pages get their default card):
A few notes on the real version of this file, which you can read in full in the :
- The logo is an inline SVG dropped straight into the JSX; I trimmed it above because its path data adds nothing to the explanation.
- Custom fonts have to be fetched as
ArrayBuffers and passed via thefontsoption ofImageResponse. I download four weights of Inter, cache the promise at module scope so each serverless instance fetches them at most once, and fall back to the default font if the download fails. - The route refuses to render drafts, so unpublished titles never leak through a guessable OG URL.
Step 3: Point Your Metadata at the Route
The route on its own does nothing until your pages reference it. In the app router, that means returning the OG image URL from generateMetadata (or the metadata export) of your post page. The minimal version looks like this:
This blog centralizes that logic in a shared metadata generator (), which resolves the post for the requested slug and passes /api/og?slug=${post.slug} as the image to both the Open Graph and Twitter card metadata.
Every post now advertises a unique, automatically generated card with its own title, subtitle, and category, and I never have to open a design tool when I publish.
Validating the Result
Before trusting any of this, check what the crawlers actually see. Paste a post URL into the or the platform-specific debuggers (Facebook's and LinkedIn's ). They fetch the page, resolve the og:image tag, and render the card exactly as it will appear in a feed, which catches wrong dimensions, missing tags, and slow image responses early.