Dynamic OG Image with opengraph-image.tsx in Next.js 16
Generate per-page OpenGraph social cards at the edge with opengraph-image.tsx, Satori, and JSX. No design tools needed.
opengraph-image.tsx is the Next.js App Router convention for generating per-page social card images. Drop a .tsx file in any route segment, return JSX, and Next.js renders it to a 1200x630 PNG with Satori at request time. The result is automatically wired into the page's og:image meta tag — no manual metadata work.
Tested on Next.js 16, runs on Node.js runtime.
When to Use This
- Per-blog-post OG images that include the post title and author
- Per-product OG cards on an e-commerce site
- Dynamic event/announcement cards that update without redeploying
- Personal branding cards on your portfolio
Don't use this when you need pixel-perfect designer-controlled images (use static files) or when the OG image requires complex animations or filters (Satori doesn't support them).
Code
// app/blogs/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og';
import { getBlogBySlug } from '@/lib/services/blogService';
export const runtime = 'nodejs';
export const alt = 'Blog post cover';
export const size = { width: 1200, height: 630 };
export const contentType = 'image/png';
export default async function OpengraphImage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const blog = await getBlogBySlug(slug);
const title = blog?.frontmatter.title ?? 'Untitled';
const author = 'Rabinarayan Patra';
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: '80px',
background: '#0a0a0a',
color: '#fafafa',
fontFamily: 'sans-serif',
}}
>
<div style={{ fontSize: 28, opacity: 0.6 }}>rabinarayanpatra.com</div>
<div style={{ fontSize: 64, fontWeight: 700, lineHeight: 1.1 }}>
{title}
</div>
<div style={{ fontSize: 32, opacity: 0.7 }}>by {author}</div>
</div>
),
{ ...size }
);
}The exported constants (size, alt, contentType) tell Next.js how to wire the file into metadata. The default function returns an ImageResponse, which Next.js renders with Satori.
Usage
The file goes inside the route segment whose page it represents:
app/
└── blogs/
└── [slug]/
├── page.tsx
└── opengraph-image.tsx ← this file
Next.js automatically generates this meta tag on /blogs/<slug> pages:
<meta property="og:image" content="https://yoursite.com/blogs/my-post/opengraph-image">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">You don't write any metadata code yourself — the file convention handles it.
Caveats
- Satori only supports a subset of CSS. No
position: absolute(yes really), no transforms, nogapuntil recently. Stick to flexbox withjustifyContentandalignItems. Test what works. runtime = 'nodejs'is required if you read from the filesystem. The default'edge'runtime can't read MDX files. Set Node runtime explicitly when fetching frontmatter.- Custom fonts must be loaded explicitly. Pass them via the
fontsoption ofImageResponse. Otherwise Satori falls back to a generic sans-serif that looks bad. - The image is regenerated on every request. That's fine for ISR pages, expensive for high-traffic dynamic ones. Cache aggressively at the CDN layer if needed.
- Image dimensions matter. 1200x630 is the OG standard. Anything else works but social platforms may crop it ugly. Don't get creative with sizing.
- Test with the Vercel OG playground. It's the easiest way to iterate without redeploying.
Related Snippets & Reading
- App Router Metadata Generator(coming soon) — for the rest of the metadata story
- Cache Components with revalidateTag — caching the underlying data fetch
- Next.js opengraph-image docs — official reference
Frequently Asked Questions
What is opengraph-image.tsx in Next.js?
It's a special file convention in the Next.js App Router. Place an opengraph-image.tsx (or .jpg/.png) inside any route segment, and Next.js automatically generates the corresponding og:image meta tag for that segment. The .tsx version uses Satori to render JSX to a 1200x630 PNG at the edge.
Why use Satori instead of a real headless browser?
Satori is built for this exact use case: convert JSX + Tailwind-like styles to an SVG, then to a PNG, with no Chrome runtime. It's faster, cheaper, and runs at the edge. The trade-off is that you only have a subset of CSS — no transforms, no animations, no complex layouts.