Use case · By SnapshotFlow Team · Updated

Dynamic Open Graph Image Generation in 2026

Every blog post, product page and changelog entry deserves a tailored social card — but maintaining a Photoshop pipeline for thousands of URLs is not a job anyone wants. This guide covers the three production patterns that actually scale in 2026: rendering an HTML template through a screenshot API, capturing a public /og route on your own site, and exposing the same operation as an MCP tool an AI agent can call directly. Each pattern ships with working code, a clear cost profile, and the cases where it wins.

TL;DR — one call

curl "https://api.snapshotflow.com/screenshot?url=https://yoursite.com/og/post-slug&width=1200&height=630&cache_ttl=2592000" \
  -H "X-Api-Key: $SNAPSHOTFLOW_KEY" \
  --output og.png

# Returns image/png, 1200x630, ready to drop into <meta property="og:image">
What you skip: Photoshop, Figma exports, a build-time pre-generation step, and a custom Edge function. What you get: an HTTP endpoint that renders any URL at 1200×630, returns a PNG, and caches for 30 days. Same endpoint as the rest of the SnapshotFlow API — see What Is a Screenshot API? for the architecture.

Why dynamic OG images still matter in 2026

Open Graph and Twitter Card images are the only artwork a reader sees before deciding whether to click. A tailored card — title, author, brand mark — tends to outperform a generic favicon in social previews, and the same artwork is reused every time the link is reshared, so the production cost amortises quickly.

The catch is volume. A medium-sized SaaS site easily has thousands of distinct shareable URLs once you count blog posts, changelogs, docs sections, customer stories and pricing tiers. Designing each card by hand in Figma does not scale. A single static template repeated everywhere looks generic. The middle ground — rendering a template on demand, parameterised by the URL — is what most production sites now ship.

The architectural question is how you render it. The three patterns below are the three answers that have stabilised in production. They are not equivalent — each has a clear sweet spot.

The three patterns at a glance

1. HTML template — render JSX/HTML directly to PNG

You write an HTML or JSX template, the renderer rasterises it to a PNG without ever serving an HTML page to the browser. Best known via @vercel/og (which uses Satori under the hood). Fast and Edge-friendly, but limited CSS subset.

Fast (50–150 ms)Edge-runtimeCSS subset only

2. URL screenshot — capture a public /og/[slug] route

You build a normal HTML route at /og/post-slug that renders the card at 1200×630. A screenshot API captures that route and returns the PNG. Full browser CSS, real fonts, web components, gradients — anything that works in Chromium works here.

Full CSS / fontsTemplate lives in your repo~1–2 s cold render

3. MCP tool — AI agent picks template + variables

The screenshot call is registered as an og_image MCP tool. An AI agent reads the post, picks a variant (light/dark, with-quote, with-author-photo), fills in the slots and gets a PNG back — all without bespoke editorial tooling.

Zero editorial workFirst-mover patternCost = 1 LLM call + 1 screenshot

Pattern 1 — HTML template (Satori / @vercel/og style)

This is the pattern made popular by @vercel/og. You write a JSX function that returns the card, the runtime converts it to an SVG via Satori, then to a PNG. No headless browser is involved, so latency is low (50–150 ms) and the cost on serverless is small.

Here is a minimal Next.js 16 App Router endpoint that renders a per-post OG image:

// app/og/[slug]/route.tsx
import { ImageResponse } from 'next/og';

export const runtime = 'edge';

export async function GET(req: Request, { params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);

  return new ImageResponse(
    (
      <div style={{
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
        width: '100%', height: '100%', padding: 64,
        background: 'linear-gradient(135deg, #0f172a, #0284c7)',
        color: '#fff', fontFamily: 'Inter',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 12, fontSize: 28, opacity: 0.85 }}>
          <span>SnapshotFlow</span> · <span>Blog</span>
        </div>
        <div style={{ fontSize: 72, fontWeight: 700, lineHeight: 1.1 }}>{post.title}</div>
        <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: 28 }}>
          <span>{post.author}</span>
          <span>{post.date}</span>
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

Wire <meta property="og:image" content="https://yoursite.com/og/{slug}" /> in your page <head> and you are done. The Edge runtime caches each unique URL, so the renderer fires once per slug.

The trade-off is the CSS subset. Satori implements a careful selection of flexbox, basic typography and gradients, but skips many properties Chromium supports natively — @container queries, custom variable fonts beyond a few weights, SVG filters, web components, advanced text-wrap modes. If your design fits inside that subset, this pattern is unbeatable on latency.

Pattern 2 — URL screenshot of a public /og route

The screenshot pattern flips the architecture. You render the OG card as a normal HTML page at a normal URL — same framework, same CSS pipeline, same fonts as the rest of your site — and then ask a screenshot API to capture that URL at 1200×630.

Step 1: build the public route. Any framework will do; here is the same Next.js 16 example, but as a page rather than an ImageResponse:

// app/og/[slug]/page.tsx
import './og.css'; // your real site CSS, custom fonts, container queries, all of it

export const dynamic = 'force-static';

export default async function OgPage({ params }: { params: { slug: string } }) {
  const post = await getPostBySlug(params.slug);
  return (
    <div className="og-card og-card--feature">
      <header className="og-card__brand">
        <img src="/brand/mark-white.svg" width={48} height={48} alt="" />
        <span>SnapshotFlow Blog</span>
      </header>
      <h1 className="og-card__title">{post.title}</h1>
      <footer className="og-card__meta">
        <span>{post.author}</span><span>{post.date}</span>
      </footer>
    </div>
  );
}

Step 2: point SnapshotFlow at it and use the returned URL as your og:image:

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }: { params: { slug: string } }) {
  const og = new URL('https://api.snapshotflow.com/screenshot');
  og.searchParams.set('url', `https://yoursite.com/og/${params.slug}`);
  og.searchParams.set('width', '1200');
  og.searchParams.set('height', '630');
  og.searchParams.set('full_page', 'false');
  og.searchParams.set('cache_ttl', '2592000');  // 30 days
  og.searchParams.set('api_key', process.env.SNAPSHOTFLOW_KEY!);

  return {
    openGraph: {
      images: [{ url: og.toString(), width: 1200, height: 630 }],
    },
    twitter: { card: 'summary_large_image', images: [og.toString()] },
  };
}

That is the whole pattern. Every shareable URL gets a tailored, fully-styled card. The first time someone shares the link, the renderer captures the route, caches the PNG for 30 days, and serves it from cache to every subsequent scraper request. CDN caching layered on top makes the per-image origin cost negligible at scale.

The two reasons people pick this pattern over @vercel/og: full CSS fidelity (whatever renders in Chromium renders here, full stop), and portability — the template lives in the same repo as the rest of your site and ships through normal PRs, with no Vercel-specific runtime dependency.

5-step recipe for the URL-screenshot pattern

  1. Build a public /og/[slug] route. Render the title, author, date and brand mark in plain HTML/CSS at the OG-canonical 1200×630 viewport. No JavaScript runtime cost — server-rendered HTML only.
  2. Capture it with /screenshot. Call GET /screenshot with width=1200, height=630, full_page=false and the rendered route's URL. SnapshotFlow returns a PNG synchronously.
  3. Set og:image in your page <head>. Point og:image at https://api.snapshotflow.com/screenshot?… with cache_ttl=2592000 so the same URL returns the same PNG for 30 days.
  4. Bust the cache by changing one query param. When the title or author changes, bump a v= query string in og:image. The new URL hits the renderer once, then is cached again by CDN, Twitter and Facebook scrapers.
  5. (Optional) Expose as an MCP tool. Register the same call as the og_image MCP tool — see Pattern 3 below — so an agent can pick the template variant and slot values autonomously.

Pattern 3 — MCP tool an AI agent can call

The third pattern reflects a shift in editorial workflows: increasingly, an AI agent reads a post, chooses a template variant and produces the finished asset in one autonomous step. The bottleneck stops being rendering — it becomes the tooling surface the agent can call.

SnapshotFlow exposes the same /screenshot operation as an MCP tool named og_image. The agent sees it as a structured function with typed arguments — title, subtitle, variant, author, date — and gets back a PNG URL.

Register the MCP server in any compliant client (Claude Desktop, Cursor, Goose, Continue):

{
  "mcpServers": {
    "snapshotflow": {
      "url": "https://api.snapshotflow.com/mcp",
      "headers": { "X-Api-Key": "sk_live_xxxxxxxx" }
    }
  }
}

Once connected, an editor can prompt the agent in plain language — "Read the new post at /blog/incident-2026-05, pick a sober dark-mode template, and post the OG card to our #marketing-launch Slack channel." The agent reads the post, calls og_image(url, variant="dark-quote", title=..., subtitle=...), gets a PNG URL, then chains into the Slack MCP. Total developer work in the editorial pipeline: zero.

Why this matters: the OG image is one of the first editorial artifacts where the decision (which template fits this post?) is small and judgement-based while the execution (render + share) is pure plumbing. MCP is the seam where the two meet, so the agent can handle the whole task end-to-end without bespoke editorial tooling.

Which pattern to pick

PatternBest when…Latency (cold)CSS supportStack lock-in
1 — HTML template (Satori)You ship on Vercel, designs are JSX-driven, latency below 200 ms matters50–150 msSatori subsetTied to Edge runtime
2 — URL screenshotYou want full Chromium CSS, custom fonts, gradients, web components; templates ship via normal PRs1–2 sFull ChromiumNone — any framework, any host
3 — MCP toolEditorial workflow already uses an AI agent; you want the agent to pick template + slot values autonomously~2 s + 1 LLM callInherits pattern 2Requires MCP-capable client

The three patterns are not mutually exclusive. The common production combination is Pattern 2 for the editorial site (full CSS, template in repo) and Pattern 3 layered on top for the actual editor — same renderer, two surfaces.

Caching strategy that survives a viral link

OG images face an unusual traffic shape: 99 % of URLs are never shared, and the 1 % that go viral get scraped hundreds of times per second by Facebook, X, LinkedIn, Slack, Discord, iMessage and a long tail of preview bots. Three layers absorb that:

  • SnapshotFlow cache_ttl. Pass cache_ttl=2592000 (30 days). The same query-string URL returns the same cached PNG without re-rendering until TTL expires.
  • CDN in front. Put Cloudflare / Bunny / Vercel in front of the screenshot URL. The CDN edge serves the PNG to scrapers without ever calling the origin renderer.
  • Cache busting on content change. When the title changes, bump a v=2 query string. New URL hits the renderer once, then caches again.
// stable URL until title changes
https://api.snapshotflow.com/screenshot?
  url=https://yoursite.com/og/post-slug&
  width=1200&height=630&
  cache_ttl=2592000&
  v=2025-05-29-r3

At 30-day TTL plus CDN, a site with 100 k indexable URLs but only 5 k actually shared ends up paying for 5 k renders per month — not 100 k. The economics are why this pattern works at scale.

vs @vercel/og / Bannerbear / Placid

ToolTemplate livesCSS supportEditorial UIMCP-ready
@vercel/ogYour repo (JSX)Satori subsetNoNo
Bannerbear / PlacidVendor canvas editorVendor rendererYes (drag-and-drop)No
SnapshotFlow /screenshotYour repo (HTML/CSS)Full ChromiumNo (use your CMS)Yes (og_image MCP tool)

None of those is wrong — they aim at different teams. Pick @vercel/og if you are already on Vercel and the CSS subset is fine; pick Bannerbear if your marketing team wants a canvas UI; pick the screenshot pattern when designers write CSS and the template needs to live in the same repo as the rest of the site, or when an AI agent is doing the work.

Production checklist

  • Render at 1200×630 exactly. X (Twitter) crops 15 px top and bottom on some clients; keep focal content inside a 1200×600 safe area.
  • Pass full_page=false. You want the viewport capture, not whatever might overflow below the fold.
  • Set cache_ttl=2592000 (30 days). Bust by bumping a v= query string on content change.
  • Put a CDN in front. Cloudflare / Bunny / Vercel — every scraper request should hit edge cache, not the renderer.
  • Test with the Twitter / Facebook validators. Both reveal whether scrapers see your card and how they crop it.
  • Fall back gracefully. If the renderer is down, serve a static brand card from the same path via CDN stale-if-error.
  • Avoid auth on the /og route. The renderer hits it like any user; auth or geo-fencing on the OG page will block it.
  • Log X-Request-Id. Every /screenshot response carries one — quote it when escalating.

FAQ

Why not just use @vercel/og?

@vercel/og is excellent when you host on Vercel and your templates are JSX-driven. It runs in the Edge runtime and renders SVG via Satori. The URL-screenshot pattern complements it: you keep full CSS — gradients, custom fonts, web components, even @container queries — because the renderer is real Chromium, not Satori. Pick @vercel/og for low-latency JSX; pick the screenshot pattern when design fidelity matters or you don't run on Vercel.

What size should an OG image be?

1200×630 is the canonical Open Graph size and works for Facebook, LinkedIn, Slack, Discord and X (Twitter) summary_large_image. Capture at that exact viewport. Keep the focal content inside the central 1200×600 area — X crops 15 px top and bottom on some clients.

How do social scrapers handle a query-string image URL?

All major scrapers (Facebook, X, LinkedIn, Slack) follow query strings and respect the response Content-Type and cache headers. SnapshotFlow returns image/png with a configurable Cache-Control, which scrapers honour. To force a refresh after a title change, bump a v= query param.

Do I need to pre-generate OG images at build time?

No. Generate on first request, cache aggressively. With cache_ttl=2592000 (30 days) and CDN caching in front, the renderer fires once per OG URL — even sites with 100 k posts only ever render the cards that actually get shared.

Can an AI agent generate the OG image itself?

Yes — that is Pattern 3. Expose the screenshot call as the og_image MCP tool. The agent reads the post title, picks a template variant (light/dark/feature/quote), fills in the slots and gets a PNG back, all in one tool call. No bespoke HTTP plumbing in your editorial workflow.

How is this different from Bannerbear or Placid?

Bannerbear / Placid let you design templates in their UI, then call a REST API to fill them in. The screenshot pattern uses your own HTML/CSS, hosted on your own domain, so the template lives in the same repo as the rest of the site and ships through normal PRs. Both approaches are valid; the screenshot pattern wins when your team is already comfortable writing HTML and CSS.

Try SnapshotFlow free

200 screenshots per month, MCP-ready, Docker self-host available. No credit card.