Respect first. Satori — the engine inside @vercel/og — is brilliant engineering. JSX goes in, SVG comes out, and no browser exists anywhere in that pipeline. Tens of thousands of projects generate their OG images with it, and it keeps getting better: HarfBuzz text shaping, CSS variables, masks and filters all landed in recent releases. When it renders your card, it does so in milliseconds, for free, inside your own deployment.

So no, this post is not "satori bad". It's a map. Satori has deliberate design boundaries, and the failure mode we hear about is never "it broke" — it's "we hit a wall three weeks in, after the templates were already designed." Here is where the walls stand, checked against the README and release notes as of satori 0.33, August 2026.

What @vercel/og actually is

@vercel/og wraps satori and an SVG-to-PNG rasterizer behind one ImageResponse API. You hand it a JSX element tree. Satori lays it out with Yoga — the flexbox engine React Native uses — and draws SVG. No Chromium. No headless anything. That one decision produces everything the tool does well, and every wall below.

Where it shines

  • Speed. Pure computation. A card renders in milliseconds on an edge function, with no browser to start.
  • Cost. Runs inside your own deployment; nobody bills you per render.
  • Integration. First-class in Next.js — one file, one route, done.
  • Determinism. Same input, same bytes, no browser version drift.

A title-plus-gradient card on a Next.js site? Right tool. Full stop. Nothing here should talk you out of it.

The walls, specifically

Everything below comes from satori's own documentation rather than our imagination, version-checked 2026-08-26.

No CSS grid. Layout is Yoga, which means flexbox only — display: flex, block, contents, none. A design your team ships as grid must be re-thought into nested flex columns. Feasible, sure. But now you maintain a translation, not a template.

No calc(), no z-index, no 3D transforms. SVG paints in document order, so layered compositions get restructured instead of stacked. Spacing systems built on calc() simply don't carry over.

No WOFF2. TTF, OTF and WOFF are accepted — loaded manually, passed in as buffers. Modern font pipelines ship WOFF2 and nothing else, which sends you hunting for alternate builds of your brand font, then counting their bytes against the function's bundle limit.

Emoji aren't built in. Each emoji is a grapheme you map to an image through graphemeImages, or resolve at runtime with loadAdditionalAsset. It works. It's also an asset pipeline you own now.

JSX only — and only the subset. No <style> tags, no external stylesheets, no scripts, stateless components only. The brand header you already built and the pricing card that already looks right can't be dropped in; everything gets rewritten into satori's dialect. That rewrite is exactly where the phrase "JSX subset wall" in our support inbox comes from.

It doesn't render web pages. The categorical one. Satori draws element trees you construct — it cannot navigate to a URL. If the image you need is "this actual page, as users see it" — a dashboard, a storefront, a competitor's pricing table — satori has no answer at all.

Parity is explicitly not guaranteed. The docs say it plainly: output is not guaranteed to match browser-rendered HTML, because satori implements its own layout engine against the SVG 1.1 spec. Close is the goal. Pixel-identical is not the contract.

The other path: a real browser

A browser-based rendering API — ours, or any equivalent — takes the opposite trade. Our OG templates are plain HTML and CSS rendered by actual Chromium: full CSS including grid, web fonts as they ship, emoji from real font stacks, and output that matches what a browser shows because it is what a browser shows. Since the engine navigates, screenshots of real URLs come from the same API call rather than a different product category.

The honest costs, to keep this fair: it's a hosted service with per-render pricing instead of code in your repo. There's a network round-trip. And you're trusting our uptime rather than your own. We keep the browser patched and guarded against SSRF, and we eat the babysitting — a trade that's its own post.

Choosing without regret

Reach for @vercel/og when:

  • The design is flex-shaped and purpose-built for cards
  • You already live in the Vercel/Next.js ecosystem
  • Render volume is high and the budget is zero
  • Fonts exist as TTF/OTF and emoji needs stay modest

Reach for a real browser when:

  • Existing HTML/CSS should be reused, not rewritten
  • The design wants grid, layering, calc(), or exact browser parity
  • The source of truth is a live URL rather than a template
  • The people maintaining templates are designers who write CSS, not JSX

When the wall arrives, it tends to arrive as a redesign request: "can we just make the image look like the page?" With a template engine, that answer is a rewrite. With a browser, it's a GET request — 100 free renders will tell you which side of the wall your design lives on.

Comparison verified against satori 0.33.4 documentation on 2026-08-26. Satori moves fast — check their README for current support before deciding on our word alone.