tool
Satori
Satori
Vercel’s JSX → SVG renderer. Takes React-like elements + a font set, returns an SVG string. Originally built for @vercel/og (OpenGraph image generation in Next.js Edge), but works as a standalone library — pair with sharp for SVG → PNG.
Core idea
Server-side React rendering that produces a vector image, not HTML. No browser, no headless Chrome. Layout uses a Yoga-flexbox subset (Satori embeds the Yoga WASM). Fonts must be supplied as ArrayBuffers — no system font fallback.
Why it exists
Replaces Puppeteer/Playwright for image generation:
- No browser binary — runs in Edge runtime, Lambda, anywhere Node runs
- Predictable — same input → byte-identical output (good for caching)
- Fast — milliseconds vs seconds for a Puppeteer cold start
- Cheap — no Chrome RAM tax
Limitations (the cost of no browser):
- Subset of CSS only — flexbox layout (Yoga), no grid, no JS, no animations, no gradients in some versions, no
position: sticky - Fonts must be loaded explicitly —
fetch().then(r => r.arrayBuffer())for every weight/family - No HTML — no
<form>, no<input>, no<canvas>. Only layout primitives. - No web fonts via
@font-faceURL — must load yourself
Typical setup
import satori from 'satori'
import sharp from 'sharp'
const fontData = await fetch(fontUrl).then(r => r.arrayBuffer())
const svg = await satori(
<div style={{ display: 'flex', width: 600, height: 800, ... }}>
<img src={bgUrl} style={{ ... }} />
<span style={{ fontFamily: 'Inter', ... }}>{text}</span>
</div>,
{
width: 600,
height: 800,
fonts: [{ name: 'Inter', data: fontData, weight: 400, style: 'normal' }],
}
)
const png = await sharp(Buffer.from(svg)).png().toBuffer()
Common usage shapes
- OG images (
@vercel/og) — wraps Satori for Next.jsroute.tsxfiles, default flow. - PDF/print output — render JSX, convert to PNG, embed or print.
- Programmatic art / charts — anywhere you need a known-pixel-size image without a DOM.
Used in
- kulevents — primary print layout renderer at
/layout-image/[layoutId]route handler. Layouts are stored asRecord<id, LayoutElement>(text + image elements with percent-based positions); Satori reads the doc, lays elements absolutely, sharp converts SVG → PNG. Fonts pre-loaded into a font map keyed by font family name.
Gotchas seen in practice
- Font loading is the long pole — every weight + style combo of every font is a separate buffer. Build a
fontMap.tsthat loads them once at module init. - Image URLs need to be absolute — Satori fetches them server-side; relative URLs and signed URLs both work, but the host must be reachable from the runtime.
- Layout silently degrades — unsupported CSS doesn’t error, just gets ignored. Visual diff between Satori output and design comp is the only ground truth.
- Percent-based positioning needs a sized parent — Satori’s flexbox needs explicit
width/heightsomewhere up the tree. sharpadds native deps — heavier on Vercel cold-start than pure-Satori; consider returning SVG directly if the consumer can render it.
Alternatives
- Puppeteer/Playwright — full browser fidelity, slow, heavy
- node-canvas — imperative Canvas API server-side, no JSX, works for simple cases
- html-to-image — client-side DOM → image; useful for “share this view” UX, no good server-side
- node-html-to-image — Puppeteer wrapper with a friendlier API
Satori is the best pick when output dimensions are known + layout is JSX-expressible + you want server-rendered, cacheable, browser-free image generation. See server-side-image-generation for the broader decision frame.
See also
- server-side-image-generation — concept page covering when to reach for Satori vs alternatives
- kulevents — production usage