Satori + React: OG Images Without a Headless Browser
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.
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:
| Stage | Input | Output | Library |
|---|---|---|---|
| 1. Layout | React element + CSS subset | SVG string | satori |
| 2. Rasterize | SVG string | PNG buffer | @resvg/resvg-js (or WASM) |
| 3. Serve | PNG buffer | HTTP image response | your 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:
| Feature | Supported? | Notes |
|---|---|---|
| Flexbox | Yes | The only layout model; root must be flex |
| Absolute positioning | Yes | Good for badges, watermarks, corner logos |
| CSS Grid | No | Rebuild grids with nested flex |
float, display: block/inline | No | Only flex and none exist |
| Fonts | Required | No system fonts — you must pass font data |
| Gradients, borders, transforms | Yes | Linear/radial gradients and border-radius work |
box-shadow, filter | Partial | Limited; 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:
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.