Dynamic OG Images: How to Generate Custom Social Cards Per Page
Learn how to generate dynamic Open Graph images per page using Next.js ImageResponse, Vercel OG, Satori, Cloudflare Workers, and other tools — with code examples.
Why use dynamic OG images?
A static OG image is the same for every page on your site. Dynamic OG images are generated on-the-fly per page — showing the article title, author, date, or any other content. This dramatically increases click-through rates because each link preview looks unique and relevant to the specific content being shared.
Sites like Vercel, GitHub, and DEV.to generate dynamic OG images. Each article, repository, or profile gets a distinct card with custom text and branding. Here's how to build this yourself.
Option 1: Next.js ImageResponse (recommended)
Next.js 13+ includes next/og, which generates images from React JSX using the Satori library. Create an opengraph-image.tsx file in any app directory:
// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'
export const size = { width: 1200, height: 630 }
export const contentType = 'image/png'
export default async function Image({ params }: { params: { slug: string } }) {
const post = await getPost(params.slug)
return new ImageResponse(
(
<div
style={{
background: '#0a0a0a',
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
justifyContent: 'flex-end',
padding: '60px',
}}
>
<div style={{ color: '#a855f7', fontSize: 20, marginBottom: 16 }}>
My Blog
</div>
<div style={{ color: 'white', fontSize: 60, fontWeight: 900, lineHeight: 1.1 }}>
{post.title}
</div>
<div style={{ color: '#a1a1aa', fontSize: 24, marginTop: 24 }}>
{post.author} · {post.publishedAt}
</div>
</div>
),
{ ...size }
)
}Next.js automatically wires up the og:image meta tag for this route. You get a unique image for every blog post with zero manual work.
Option 2: API route with Satori
For more control, use satori directly to generate SVG images in an API route:
// pages/api/og.tsx (Next.js Pages Router)
import satori from 'satori'
import sharp from 'sharp'
import { NextApiRequest, NextApiResponse } from 'next'
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
const { title = 'My Site' } = req.query
const svg = await satori(
<div style={{ display: 'flex', background: '#0a0a0a', width: 1200, height: 630 }}>
<h1 style={{ color: 'white', fontSize: 60, padding: 60 }}>{title}</h1>
</div>,
{
width: 1200,
height: 630,
fonts: [
{
name: 'Inter',
data: fs.readFileSync('./public/fonts/Inter-Bold.ttf'),
weight: 700,
},
],
}
)
const png = await sharp(Buffer.from(svg)).png().toBuffer()
res.setHeader('Content-Type', 'image/png')
res.setHeader('Cache-Control', 'public, max-age=86400')
res.send(png)
}Option 3: Cloudflare Workers
For edge-rendered OG images, Cloudflare Workers with the @cloudflare/pages-plugin-sentry or direct Worker scripts can generate images at the edge with very low latency:
// functions/og.ts (Cloudflare Pages Function)
import { ImageResponse } from '@cloudflare/pages-plugin-og'
export async function onRequest({ request }) {
const url = new URL(request.url)
const title = url.searchParams.get('title') ?? 'My Site'
return new ImageResponse(
<div style={{ background: '#0a0a0a', width: 1200, height: 630, display: 'flex' }}>
<h1 style={{ color: 'white', fontSize: 60, padding: 60 }}>{title}</h1>
</div>,
{ width: 1200, height: 630 }
)
}Option 4: Third-party OG image services
If you don't want to manage your own image generation, services like og-image.vercel.app, bannerbear.com, and placid.app provide API-based dynamic image generation. Pass URL parameters and get a unique image back:
<!-- Dynamic OG image via URL parameters --> <meta property="og:image" content="https://og-image.vercel.app/My%20Post%20Title.png?theme=dark&md=1" />
Best practices for dynamic OG images
- Cache generated images: add a
Cache-Controlheader with a long TTL (at least 24 hours) to avoid regenerating on every scrape. - Use 1200×630 px: this is the optimal size for all major platforms.
- Keep text short: titles longer than 60-70 characters get cut off in most card previews.
- Embed fonts: remote font loading may not work in Satori. Bundle font files locally.
Preview your OG tags free at OGFixer.com → Paste your URL to preview your dynamic OG image on Twitter, LinkedIn, Discord, and Slack side by side.