How to Generate Dynamic Open Graph Images from a URL
Generate a URL-specific Open Graph image with Next.js or a Vercel Function, connect it to page metadata, and make sure crawlers can fetch it.
To generate a dynamic Open Graph image from a URL, render a social card at a public image endpoint using the page’s data, then put that endpoint’s absolute URL in the page’s og:image metadata. In Next.js App Router, the direct option is an opengraph-image.tsx file in the route segment. For a reusable endpoint, use @vercel/og in a Vercel Function. The examples below render a card from trusted page data; they do not take a live screenshot of the referenced page.
1. Choose the right rendering approach
Use a metadata image route when each page owns its own social image and you use Next.js App Router. Choose a reusable function when several routes or applications need to request a card from one endpoint. Both approaches render a designed template from data.
| Approach | Best for | Tradeoff |
|---|---|---|
Next.js opengraph-image.tsx |
Images tied to a Next.js route and its data | Fits the metadata convention; cache behavior follows Next.js route rules |
Vercel Function with @vercel/og |
A reusable endpoint with changing content | You design parameter validation, deployment, and caching |
| Browser screenshot service or pipeline | The image should reproduce an actual rendered webpage | Uses browser rendering; plan for browser execution and cache strategy |
Vercel recommends a 1200×630 image. Its OG renderer uses Satori and Resvg to convert JSX-like markup into PNG. It supports a subset of CSS, with common layouts and text wrapping, but not CSS Grid. Satori uses its own layout engine, so the output is not guaranteed to match browser-rendered HTML pixel for pixel. See the Vercel OG image generation documentation and the Satori project documentation.
2. Generate a card in Next.js App Router
Create app/articles/[slug]/opengraph-image.tsx. Fetch the article using the same trusted data source as the page, then render a fixed-size design. This example uses a local lookup to make the route runnable without an external CMS.
import { ImageResponse } from 'next/og'
export const size = {
width: 1200,
height: 630,
}
export const contentType = 'image/png'
export const alt = 'Article social sharing image'
const articles: Record<string, { title: string; author: string }> = {
'dynamic-og-images': {
title: 'How to Generate Dynamic Open Graph Images',
author: 'Engineering',
},
}
export default async function OpenGraphImage({
params,
}: {
params: Promise<{ slug: string }>
}) {
const { slug } = await params
const article = articles[slug]
if (!article) {
return new Response('Not found', { status: 404 })
}
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'space-between',
padding: 72,
background: '#101827',
color: '#ffffff',
fontSize: 64,
fontWeight: 700,
}}
>
<div style={{ color: '#8db8ff', fontSize: 28 }}>ENGINEERING</div>
<div style={{ display: 'flex', lineHeight: 1.12 }}>{article.title}</div>
<div style={{ color: '#c6d0df', fontSize: 28 }}>{article.author}</div>
</div>
),
size,
)
}
In current Next.js App Router versions, route params are asynchronous as shown. Check the installed Next.js API reference if using an older version. The special metadata image file convention emits the route as an image; the image URL is based on the route segment, commonly /articles/dynamic-og-images/opengraph-image.
Make sure the page emits matching metadata. For a fixed page URL, a metadata object can point directly at the route:
export const metadata = {
openGraph: {
images: ['/articles/dynamic-og-images/opengraph-image'],
},
}
If metadata depends on a slug, return it from generateMetadata and use the absolute canonical page origin when constructing the image URL. Next.js also supports generated metadata through its file convention. See Next.js opengraph-image reference.
3. Build a reusable Vercel Function endpoint
Install the package in a Vercel project and expose an endpoint that validates the requested content before rendering. The example uses a fixed allowlist so callers cannot submit arbitrary markup or remote resources.
npm install @vercel/og
// api/og.ts
import { ImageResponse } from '@vercel/og'
export const config = {
runtime: 'edge',
}
const cards: Record<string, { title: string; section: string }> = {
'dynamic-og-images': {
title: 'How to Generate Dynamic Open Graph Images',
section: 'Engineering guide',
},
}
export default function handler(request: Request) {
const url = new URL(request.url)
const slug = url.searchParams.get('slug') ?? ''
const card = cards[slug]
if (!card) {
return new Response('Unknown card', { status: 404 })
}
return new ImageResponse(
(
<div
style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 72,
background: '#102238',
color: 'white',
}}
>
<div style={{ fontSize: 28, color: '#9dc9ff' }}>{card.section}</div>
<div style={{ marginTop: 32, fontSize: 64, fontWeight: 700 }}>{card.title}</div>
</div>
),
{ width: 1200, height: 630 },
)
}
Deploy the function and reference its public URL from the page metadata, for example https://example.com/api/og?slug=dynamic-og-images. Ensure the route returns an image response and a successful status. The endpoint alone does not attach an image to a shared page; the page must expose the URL through og:image.
4. Add the Open Graph metadata
For a plain HTML page, put these tags in the document head, substituting the canonical public page URL and the actual image URL:
<meta property="og:title" content="How to Generate Dynamic Open Graph Images">
<meta property="og:type" content="article">
<meta property="og:url" content="https://example.com/articles/dynamic-og-images">
<meta property="og:image" content="https://example.com/articles/dynamic-og-images/opengraph-image">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="A social card for the dynamic Open Graph image guide">
Use an absolute, publicly reachable URL for crawlers. Add image dimensions and useful alternative text. If the card is generated from a user-provided URL, treat that value as untrusted input: map known page IDs or slugs to server-side data, cap text lengths, escape content through the rendering framework, and do not fetch arbitrary URLs without appropriate controls.
5. Handle data, fonts, and layout constraints
Use trusted page data
Prefer a slug or stable content identifier and load the title and author from your application data source. Do not accept arbitrary JSX, HTML, or a URL to fetch as the image template. Return 404 for missing content and avoid silently rendering a misleading fallback card.
Keep the layout inside the supported CSS subset
Use flexbox-based composition, explicit dimensions, spacing, colors, and font sizes. Do not rely on CSS Grid, browser-only layout behavior, external stylesheets, or complex effects without checking the renderer support. Test long titles, non-Latin glyphs, emojis, and missing fonts. Include the required font assets where the rendering runtime expects them.
Decide how changes affect the image
Next.js says generated metadata images are statically optimized and cached by default unless dynamic APIs, uncached data, or dynamic route configuration changes that behavior. If article title updates must immediately change the card, configure revalidation or dynamic behavior deliberately. If the content is stable, caching avoids repeated generation. See the metadata image caching guidance.
6. Make the image fetchable by social crawlers
- Deploy the page and image endpoint to a public HTTPS host.
- Request the page as a crawler would and confirm its HTML includes the intended absolute
og:imageURL. - Request the image URL directly and confirm it returns a successful image response, not an authentication page, redirect loop, or HTML error document.
- Check that robots rules permit access to the page and generated image path. Vercel specifically recommends allowing OG image API routes in
robots.txt; this recommendation cannot guarantee behavior across every social platform. - After changing metadata or the image, account for platform and application caches. Use a versioned image URL when you need a new cache key, while keeping page metadata consistent.
# Check the metadata response
curl -sS https://example.com/articles/dynamic-og-images | grep -i 'og:image'
# Check that the image route is reachable and inspect response headers
curl -sS -D - -o /tmp/card.png 'https://example.com/api/og?slug=dynamic-og-images'
file /tmp/card.png
7. When you need a screenshot of the actual page
A generated OG card is a designed image assembled from page data. If the requirement is to capture the live rendered page as it appears in a browser, use a browser screenshot pipeline and point og:image at the resulting public image. ScreenshotNeo is a website screenshot API and MCP server; it takes a URL and returns PNG, JPEG, WebP, or PDF. Its options include full-page capture, device viewports, custom CSS and JavaScript, waiting for page conditions, and cache TTL.
Or skip the browser setup
For a live webpage capture, one GET request can return the image. See the ScreenshotNeo API documentation for parameters and response behavior.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, and failed loads are never billed. An MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. See ScreenshotNeo and the API docs.
Create a free ScreenshotNeo account for 1,000 screenshots a month, with no card required.
8. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Share preview has no image | No og:image, or its URL is relative, private, or blocked |
Emit an absolute HTTPS URL and request it without authentication. Verify robots access. |
| Image route returns 404 | Wrong route path, missing slug, or data lookup miss | Confirm the Next.js segment and metadata URL match; check that the referenced record exists. |
| Image endpoint returns HTML instead of PNG | Runtime error, deployment fallback, or unhandled request | Inspect function logs and response headers; confirm successful paths return ImageResponse. |
| Some CSS is missing or layout differs | The renderer supports only a subset of CSS and does not use the browser layout engine | Use supported flexbox styles and explicit sizing; simplify and inspect the rendered result. |
| Updated title does not appear | Next.js, CDN, or social platform cache still has the old image | Review route revalidation and cache settings; request a versioned image URL if a new cache key is needed. |
| Long title clips or overflows | Text exceeds the card bounds or wraps differently than expected | Set a maximum title length, reduce font size for long content, and test worst-case titles. |
| Characters render as boxes | Required glyphs are absent from the configured font | Bundle a font that includes the needed character range and verify runtime font loading. |
| Image works in browser but crawler cannot fetch it | Access controls, robots rules, redirect behavior, or crawler networking restrictions | Make the image route publicly fetchable, allow it in robots rules, and avoid cookie or session requirements. |
9. Performance, reliability, and cost
- Cache stable content: Generated cards can be reused until their source data changes. Set revalidation or dynamic behavior based on freshness requirements; avoid needless regeneration for every crawler request.
- Keep the template small: A fixed layout and local data reduce runtime dependencies. Remote font and image fetches add failure modes, so use only assets the renderer can reliably access.
- Validate before rendering: Bound title and description lengths, reject unknown identifiers, and handle missing records explicitly. This prevents malformed requests from producing broken cards.
- Budget for generation: This dossier establishes no benchmark or cost figure for Vercel rendering. Check current platform limits and billing for your deployment and request pattern; do not infer a per-image price from the renderer documentation.
- Keep browser capture distinct: A live page screenshot involves browser rendering and separate execution and cache considerations. ScreenshotNeo reports verdict and billing headers; only clean shots are billed, while bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing.
10. FAQ
Does generating an image automatically add it to a shared page?
No. The page’s metadata must reference the public image URL with og:image.
Can I pass any URL to make a card?
Use a trusted page identifier and your own data source. Accepting arbitrary URLs can expose the endpoint to unsafe fetches and unpredictable content.
Is a generated card the same as a website screenshot?
No. A template renderer composes a designed image from data; a screenshot captures the rendered page. Choose based on whether the card should follow your branding or reproduce the page itself.
What size should I start with?
Use 1200×630 pixels, the size recommended in Vercel’s OG image generation guide.


