tool

Satori

created 2026-05-10 image-generation · jsx · svg · vercel · og-images

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-face URL — 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

  1. OG images (@vercel/og) — wraps Satori for Next.js route.tsx files, default flow.
  2. PDF/print output — render JSX, convert to PNG, embed or print.
  3. 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 as Record<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.ts that 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/height somewhere up the tree.
  • sharp adds 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-imageclient-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