Dynamic Open Graph images are PNGs (or JPGs) produced at request time from a template—usually a title, subtitle, and brand chrome—rather than a single hand-exported file. Crawlers do not care how the bytes were made. They care that og:image returns a public image over HTTPS. This guide covers when to go dynamic, Next.js ImageResponse, a high-level look at Satori-style rendering, query URL patterns, SocialImageKit helpers, and honest notes on caching and host limits.
For App Router wiring details, see How to set an OG image in Next.js. For design habits that still apply to generated cards, see OG image best practices.
Static PNG vs dynamic image
| Approach | Best when | Trade-offs |
|---|---|---|
Static file (/og/launch.png) |
Marketing pages, rare updates | Manual export per variant |
Dynamic route (/api/og?title=… or opengraph-image.tsx) |
Blogs, docs, per-SKU cards | Runtime, fonts, caching, cold starts |
Choose static when one card is enough and designers own the pixels. Choose dynamic when titles change often and you refuse to export hundreds of PNGs by hand. A hybrid works well: static homepage art, dynamic post cards.
Either way, the HTML must emit an absolute og:image. Relative paths and client-only tags fail the same way they fail for static files (og:image not showing).
What crawlers need from a dynamic URL
og:imagepoints at a stablehttps://URL (query strings are fine if they are deterministic).- A GET to that URL returns
image/png(or JPEG) without login. - Response time is short enough that scrapers do not give up.
- Bytes are cacheable at the CDN when the inputs have not changed.
Optional width/height tags still help:
<meta property="og:image" content="https://example.com/api/og?title=Ship%20notes" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
Verify with View Source and the Link Preview Checker before you trust Facebook or LinkedIn scrapes.
Next.js ImageResponse (practical overview)
In the App Router you can add opengraph-image.tsx beside a route. The file exports size and a default function that returns ImageResponse from next/og:
import { ImageResponse } from "next/og";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";
export default async function Image({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const title = slug.replace(/-/g, " ");
return new ImageResponse(
(
<div
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
padding: 64,
background: "#0f172a",
color: "white",
fontSize: 64,
}}
>
{title}
</div>
),
{ ...size }
);
}
Keep layouts to flexbox and fonts the runtime supports. Load brand fonts explicitly when you need them. Set metadataBase so emitted tags are absolute. Full patterns: Next.js OG guide.
You can also point openGraph.images at a custom route you control. Next’s file convention is convenient because it wires tags for you.
Satori at a high level
Many modern OG pipelines (including helpers related to next/og) use a Satori-style path: describe a layout with a limited HTML/CSS subset → produce SVG → rasterize to PNG. Practical consequences:
- Prefer flex layouts over complex CSS grid or absolute chaos.
- Not every CSS property exists—prototype simply.
- Fonts must be available to the renderer; missing fonts yield fallbacks or failures.
- Embed logos as data URLs or fetchable absolute assets the worker can reach.
You do not need to call Satori by name in application code if your framework wraps it. Understanding the model helps when debugging “why won’t my CSS work in the OG route?”
Query-style /api/og URLs
A common pattern on edge platforms:
https://example.com/api/og?title=Launch&theme=dark
The handler reads query params, renders the template, and returns PNG. Benefits: one route for many pages; HTML only needs to encode the title. Risks: unbounded query cardinality, cache misses, and host mismatch.
Hosting honesty (including Hostinger-style static setups)
Cloudflare Workers examples assume a Workers-compatible deploy. If your site is static HTML on a host that does not execute Cloudflare functions (or similar serverless runtimes), copying a Worker /api/og snippet will not magically run. Options:
- Deploy the OG function on a platform that supports it, and point
og:imageat that absolute URL. - Pre-render PNGs in CI and upload static files.
- Use your framework’s built-in image route on a host that runs that framework (for example Vercel/Node for Next.js).
Pick the runtime you actually operate. Document the owner of the image URL in the repo README.
SocialImageKit: design → code → your stack
Use SocialImageKit as a design and scaffolding path, not as a claim that one hosted API must render every share for you forever:
- Design the card in the OG Image Generator (browser-side workflow for layout).
- Export implementation-oriented output from code export when you want a starting point for JSX/API-style generation.
- Use developer SDK helpers when integrating generation into your own tooling.
- Host the resulting route or static files on your infrastructure so
og:imagestays under your control.
That split keeps the free browser generator useful for mockups while dynamic production rendering stays owned by your app—with your caching and rate limits.
Preview crops with the Social Preview Simulator. Confirm dimensions against OG image size. Build matching meta snippets with the Meta Tag Generator.
Caching and rate limits (be honest)
Dynamic images fail in production for boring reasons:
- Cold starts on the first Facebook scrape after deploy
- No CDN cache on the image URL, so every messenger hits origin
- Upstream CMS fetches inside the renderer timing out
- Font downloads on every request
- Rate limits on third-party render APIs or screenshot services
Mitigations that work in practice:
- Cache at the edge by full URL (including sorted query params).
- Keep the renderer CPU-light—no headless browser if a template PNG will do.
- Ship fonts with the function bundle when possible.
- Provide a static fallback image if generation fails.
- Prefer deterministic inputs (title from the CMS slug) over free-text that explodes cache keys.
- When art/templates change, bump a version query (
v=2) or path segment so platforms fetch a new object.
Do not promise unlimited free third-party rendering. If you call an external API to rasterize, budget for quotas and own a backup path.
Security and abuse notes
Open query parameters invite abuse: huge titles, endless unique URLs, or injected markup. Sanitize length, escape text, and reject unexpected themes. Rate-limit anonymous /api/og if it is public. Never pass raw HTML from the query string into the renderer.
Implementation checklist
- Decide static vs dynamic per page type
- Canvas ~1200×630 with safe margins (best practices)
- Absolute
og:imagein the first HTML response - Runtime actually hosts the function (or you pre-render)
- Edge cache + short failure fallback
- View Source + Link Preview Checker
- Platform re-scrape after template changes (Facebook Sharing Debugger, LinkedIn Post Inspector)
- Version the image URL when the template changes
Workflow that scales for content teams
Editors should not need to open Figma for every post if engineering owns a solid template. Agree on fields the renderer accepts: title, optional eyebrow, optional author. Keep titles short enough to fit the safe zone—dynamic does not mean unlimited text. Truncate gracefully in the layout rather than letting type collide with the logo.
For docs sites, generate from the page title at build or request time and store nothing extra in the CMS. For marketing, lock the template and only allow approved themes. For product pages, pass SKU name and price carefully—prices change; consider whether the card should be timeless or time-bound.
After you ship a new template, re-test three URLs and refresh Facebook/LinkedIn scrapes. Dynamic systems make it easy to forget that platforms still cache. Changing the React tree is not enough if og:image keeps the same URL and crawlers never re-fetch.
When dynamic is the wrong tool
Skip dynamic generation when:
- You have fewer than a handful of share images
- Legal/brand requires pixel-perfect designer sign-off on every asset
- Your host cannot run functions and you do not want a second deploy target
- The “dynamic” data never changes—then a static PNG is simpler and more reliable
Dynamic OG images are a productivity tool, not a requirement. Many high-quality sites still ship hand-made 1200×630 PNGs for launches and use generation only for the blog. Start with the smallest approach that keeps cards readable, then automate when exporting by hand becomes the bottleneck.
Wire tags carefully, cache the PNG, and verify with the same discipline you use for static files. The crawler still only sees a URL and an image—make both boringly reliable.
Frequently asked questions
Short answers from this guide.
When should I use dynamic OG images instead of a static PNG?
When each page needs unique artwork—blog titles, docs, product names, or campaign parameters. Keep a static PNG for evergreen marketing pages that rarely change.
How does Next.js ImageResponse work at a high level?
An opengraph-image route (or similar) returns ImageResponse from next/og, which renders a simple JSX layout to PNG. Set size to 1200×630 and ensure the emitted og:image URL is absolute.
What is Satori used for?
Satori converts a subset of HTML/CSS-like layout into SVG that can be turned into a PNG. Framework OG helpers and many /api/og examples build on that approach. Keep flexbox layouts simple and ship supported fonts.
Can I host Cloudflare-style /api/og functions on any host?
No. Query-based OG routes need a runtime that executes your function. Static hosts (including setups that do not run Cloudflare Workers) will not serve those handlers—deploy the route where functions run, or pre-render PNGs.
Does SocialImageKit host dynamic OG images for every domain at no cost?
The browser OG Image Generator helps you design and export. Code export and the developer SDK help you implement generation in your own stack. Treat third-party render APIs as services with their own limits—plan caching and ownership of the image URL you put in og:image.
Why do dynamic OG images sometimes fail the first scrape?
Cold starts, timeouts, missing fonts, or uncached upstream data. Prefer edge caching, short timeouts, and a fallback static image so crawlers still get a valid PNG.