Dynamic OG image generation

Satori + React: OG Images Without a Headless Browser

By Social Card Studio5 min read

Satori is an open-source library from Vercel that converts a React element — your JSX and inline CSS — into an SVG, with no headless browser involved. Pair it with an SVG-to-PNG converter and you have a complete pipeline for generating Open Graph images that runs in milliseconds on the edge. This is the engine inside @vercel/og, and understanding it is the difference between fighting the renderer and shipping cards that work.

What is Satori, and why skip the browser?

For years, dynamic OG images meant launching headless Chromium (via Puppeteer or Playwright), navigating to an HTML page, and screenshotting it. It works, but it's slow, memory-hungry, and impossible on most edge runtimes. Satori takes a different path: it reads your JSX and a supported subset of CSS, computes the layout itself, and emits an SVG directly. No DOM, no browser, no screenshot.

milliseconds
is the render budget Satori targets — it parses JSX to SVG with no browser boot, fast enough to run per-request inside an edge function. Vercel, @vercel/og docs

That speed unlocks per-request generation: a unique, branded card for every post, rendered on demand and cached. The tradeoff is that Satori is not a browser — it supports the CSS you need for a card layout and nothing more.

How does the Satori pipeline work?

Three stages turn a React component into a shareable PNG:

StageInputOutputLibrary
1. LayoutReact element + CSS subsetSVG stringsatori
2. RasterizeSVG stringPNG buffer@resvg/resvg-js (or WASM)
3. ServePNG bufferHTTP image responseyour framework

@vercel/og collapses all three into a single ImageResponse call, but each stage is a standalone library you can swap. The minimal version of the pipeline looks like this:

import satori from "satori";
import { Resvg } from "@resvg/resvg-js";

const svg = await satori(
  <div style={{ display: "flex", width: "100%", height: "100%",
                alignItems: "center", justifyContent: "center",
                background: "#0b1021", color: "#f5f6fa", fontSize: 64 }}>
    Hello from Satori
  </div>,
  { width: 1200, height: 630, fonts: [{ name: "Inter", data: fontData, weight: 400, style: "normal" }] }
);

const png = new Resvg(svg).render().asPng();

That 1200×630 is deliberate: it's the universal 1.91:1 Open Graph dimension that renders correctly on Facebook, LinkedIn, Discord, and as an X large card. See the 2026 social media image size cheat sheet for the per-platform breakdown.

Build the OG image route, step by step

In a Next.js App Router project, @vercel/og wires the whole pipeline behind one component. Drop this in app/og/route.tsx:

import { ImageResponse } from "next/og";

export async function GET() {
  return new ImageResponse(
    (
      <div style={{ display: "flex", flexDirection: "column", width: "100%",
                    height: "100%", padding: 80, background: "#0b1021",
                    color: "#f5f6fa", justifyContent: "space-between" }}>
        <div style={{ display: "flex", fontSize: 28, opacity: 0.7 }}>yourblog.com</div>
        <div style={{ display: "flex", fontSize: 64, fontWeight: 700, lineHeight: 1.1 }}>
          Your post title goes here
        </div>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

The component is your template; the request can read a ?title= query param and inject it. Every element with more than one child sets display: flex — Satori requires it.

What are Satori's real limitations?

Satori implements a layout-focused slice of CSS — enough for a card, not enough for a webpage. Know the edges before you design against them:

FeatureSupported?Notes
FlexboxYesThe only layout model; root must be flex
Absolute positioningYesGood for badges, watermarks, corner logos
CSS GridNoRebuild grids with nested flex
float, display: block/inlineNoOnly flex and none exist
FontsRequiredNo system fonts — you must pass font data
Gradients, borders, transformsYesLinear/radial gradients and border-radius work
box-shadow, filterPartialLimited; test before relying on it

The font requirement trips up the most people: Satori ships with zero fonts, so you must load at least one as an ArrayBuffer or text renders as empty boxes. For non-Latin scripts or emoji, you register additional fonts or a fallback loader.

Why does any of this matter for engagement?

A dynamic card pipeline is only worth building if branded previews actually move numbers — and they do. The case for generating a real image per post, rather than shipping no card or a generic one, rests on hard data:

+114%
more impressions for posts and links that carry an image versus those with none — the baseline reason to render any OG card at all. CXL, click-through benchmarks
~+80%
recognition lift from consistent brand-color use — which a templated Satori card enforces automatically across every post. Metricool

A headless-browser screenshot service can produce the same pixels, but it can't do it per-request on the edge in milliseconds, and that latency is the difference between caching a card and timing out a scraper. For the architectural comparison of rolling your own versus an API, see dynamic social cards: API generators vs self-hosted, and for the framework-specific walkthrough, the @vercel/og Next.js guide.

The takeaway

Satori renders React to SVG with no browser, resvg turns that SVG into a PNG, and @vercel/og packages both behind one ImageResponse. Build to its CSS subset — flexbox only, fonts required, display: flex on every multi-child node — and you get fast, branded, per-post cards. If you'd rather not maintain the template, the font loading, and the edge caching yourself, Social Card Studio runs this exact pipeline and emits a branded 1200×630 card for every post automatically.

Frequently asked questions

What is Satori?

Satori is an open-source library from Vercel that converts HTML and CSS — expressed as JSX or a React element — into an SVG. Paired with an SVG-to-PNG converter like resvg, it generates Open Graph images from React without running a headless browser.

Does Satori need a headless browser like Puppeteer?

No. That's the whole point. Traditional OG generators spin up headless Chromium to screenshot a page, which is slow and heavy. Satori parses your JSX and CSS directly into an SVG, so it runs in milliseconds on edge runtimes where a browser can't.

What CSS does Satori support?

A subset focused on layout: flexbox, absolute positioning, fonts, colors, gradients, borders, and transforms. It does not support CSS Grid, floats, or display values other than flex and none. Every element with more than one child must explicitly set display: flex.

How do I turn a Satori SVG into a PNG?

Satori outputs SVG; most platforms want PNG. Convert it with @resvg/resvg-js (or the WASM build on the edge). Vercel's @vercel/og bundles Satori plus resvg behind an ImageResponse helper so you skip the conversion step entirely.

← All posts