Satori renders a subset of CSS. HTML to Image renders the web: full CSS, JavaScript, web fonts and emoji in real Chrome, from any framework.
Disclosure: we built HTML to Image. Satori is clever engineering with deliberate trade-offs; this page is about what happens when your design outgrows them.
| Factor | Satori / @vercel/og | HTML to Image |
|---|---|---|
| Cost to start | Free (open source) | $0 to start — 25 renders/month free |
| Infrastructure to run | Runs inside your functions or edge runtime | None — hosted API |
| Ongoing maintenance | Font loading, emoji setup, CSS workarounds | None |
| Rendering engine | Custom renderer — a subset of CSS, flexbox only | Headless Chrome — full CSS |
| JavaScript execution | No — static JSX/HTML only | Yes |
| Scaling & concurrency | Scales with your functions | Handled by the API |
| Named templates | No | 25 |
| CDN-hosted output | No — streams from your function each request | Yes — every render returns a CDN URL |
| Best for | simple OG cards inside a Next.js/Vercel app | full-fidelity images from real HTML, in any framework |
Stay with @vercel/og when your card genuinely fits the subset: flexbox layout, a couple of fonts you load yourself, no emoji surprises, and you are on Vercel anyway. Within those lines Satori is fast and free, and there is nothing to fix.
Switch to HTML to Image when the design outgrows the renderer. The failure modes are predictable and we have documented them: CSS grid, absolute positioning quirks and background limitations that silently render wrong, and emoji that come out as blank boxes without per-glyph font plumbing. In real Chrome those problems do not exist — the markup that works on your site works in the image, JavaScript included. And because every render returns a stored CDN URL, you render each image once instead of re-rendering on every crawler hit. The migration pattern is in our guide to dynamic OG images in Next.js without @vercel/og.
The whole CSS spec, not a subset. Grid, floats, filters, blend modes, pseudo-elements, web fonts by URL and JavaScript-driven content — if Chrome renders it, the image has it. No mental checklist of what Satori supports before you touch a design.
Emoji and international text that just work. No glyph APIs, no font subsetting, no broken flag sequences. Chrome ships the same emoji and shaping behaviour as every browser your users have.
Framework-independent with hosted output. Works from Rails, Laravel, WordPress or a static site generator — see our guides for Astro, Hugo and Eleventy and Nuxt — plus 25 named templates when you would rather skip the markup entirely.
Latency and price inside its lane. Rendering at the edge in tens of milliseconds with no external call and no per-render fee is a real advantage for simple cards generated on the fly on Vercel.
JSX ergonomics for React teams. Defining an OG card as a React component inside the repo, versioned with the code, is a pleasant workflow while the design stays inside the subset.
// @vercel/og — JSX in, PNG out, within Satori's CSS subset
import { ImageResponse } from '@vercel/og';
export default function handler() {
return new ImageResponse(
(
<div style={{ display: 'flex', fontSize: 64 }}>
Hello {/* flexbox only, no JS, emoji need setup */}
</div>
),
{ width: 1200, height: 630 },
);
} # HTML to Image — real HTML, real Chrome, full CSS
curl -X POST https://app.html2img.com/api/html \
-H 'X-API-Key: your-key-here' \
-H 'Content-Type: application/json' \
-d '{"html":"<h1>Hello 👋</h1>","css":"h1{font-size:64px}",
"width":1200,"height":630}'
# Response includes a CDN-hosted URL The migration is usually a simplification: your JSX card becomes plain HTML and CSS with no subset rules, sent to the HTML endpoint. Render at publish time rather than request time, store the returned CDN URL in front matter or your database, and the OG route handler disappears entirely.
Satori is free per render but you pay in constraints and workarounds: font files fetched and passed manually, emoji glyph services, designs re-engineered to flexbox, and debugging output that almost matches the browser. The moment a design review says "why does this look different from the site?", you are spending engineer time that costs more than $9 a month. If your cards are simple and stay simple, the free option is genuinely fine — that honesty cuts both ways.
Satori implements a deliberate subset of CSS: flexbox layout only (no grid, no floats), no JavaScript, limited background and filter support, and every element needs display: flex if it has multiple children. Real-world designs hit these walls fast — we keep a detailed list in our article on Satori's CSS limits. HTML to Image renders in actual Chrome, so if it works on a web page it works in the image.
Satori has no built-in emoji font, so emoji render as blank boxes unless you wire up an external glyph source per emoji — and combined sequences like flags and skin tones are patchy even then. We wrote up the mechanics and the fix in "Why @vercel/og fails on emoji". In headless Chrome, emoji are just text: they render correctly with zero setup.
When your design fits the subset and you are already on Vercel. For a simple OG card — solid background, a heading, a logo — Satori is genuinely fast, renders at the edge, and costs nothing extra on your existing plan. If your card never hits the CSS limits or the emoji problem, there is no urgent reason to switch.
No. You call the API from wherever you run — a Next.js route handler, a build step, or your CMS. Our guide to dynamic OG images in Next.js without @vercel/og shows the pattern: render once, store the CDN URL in your front matter or database, and stop paying a render on every crawler hit.
The free tier covers 25 renders a month with no credit card. Send the markup Satori couldn't handle and see it come back correct.
Or skip the markup entirely. Twenty-five pre-built designs — including OG image templates — rendered from JSON in real Chrome.