How to Generate Fast OG Images with Satori + Resvg in Astro
Dynamically generating Open Graph (OG) images with code is standard practice, but doing it wrong can kill your CI/CD performance or break your Edge Functions in production.
Satori simplifies design by allowing you to use JSX and Tailwind as if you were creating a regular React component. The problem: Satori doesn't generate a final image, it produces an SVG. For Twitter, LinkedIn, or Facebook to render the card correctly, you need to convert that SVG to PNG using Resvg.
Satori (JSX → SVG)
↓
Resvg WASM (SVG → PNG)
↓
Edge Function / CDNSatori draws. Resvg renders.
Satori + Resvg vs. Puppeteer
| RecommendedSatori + Resvg | Puppeteer | |
|---|---|---|
| Runtime | Lightweight, no heavy dependencies | Requires running Chromium |
| Rendering | SVG → PNG via WASM | Full browser |
| Edge Compatibility | ✓✓ | ✗ |
| Performance | Milliseconds | Higher cold starts |
| CSS | Partial support | Full support |
| Setup Complexity | Low | High |
Puppeteer remains the best option if you need full CSS support or extremely complex layouts. For most modern OG Images, Satori + Resvg gets the job done with significantly less overhead.
Implementation in Astro
import satori from "satori";
import { Resvg } from "@resvg/resvg-wasm";
import type { APIRoute } from "astro";
export const GET: APIRoute = async ({ params }) => {
const { slug } = params;
const title = slug
? slug.replace(/-/g, " ")
: "My Post";
// 1. Generate the SVG using Satori
const svg = await satori(
<div tw="flex w-full h-full bg-slate-900 items-center justify-center">
<h1 tw="text-white text-6xl font-bold">
{title}
</h1>
</div>,
{
width: 1200,
height: 630,
fonts: [
// Load font manually
],
}
);
// 2. Convert SVG → PNG
const resvg = new Resvg(svg, {
fitTo: {
mode: "width",
value: 1200,
},
});
const pngData = resvg.render().asPng();
// 3. Return a cacheable response
return new Response(pngData, {
headers: {
"Content-Type": "image/png",
"Cache-Control":
"public, max-age=31536000, immutable",
},
});
};The Real Bottleneck
The issue is rarely Satori.
The issue is the runtime.
Edge Functions have aggressive memory and CPU limits, especially on free plans. If your layout has too many layers, heavy images, or multiple custom fonts, the render might exceed the allowed execution time and return intermittent errors.
When your design starts looking like a full landing page squeezed into 1200×630, moving the endpoint to a traditional Node.js Serverless Function is the most sensible way out. You add some cold start; in return, you get plenty of CPU without the aggressive Edge limits.
The Interesting Part for GEO
OG Images control the visual distribution layer on social media. LLMs do not process this layer: they process semantic structure. That is why they serve different purposes and are not interchangeable.
| Layer | Goal |
|---|---|
| OG Images | Visual distribution and CTR |
| JSON-LD | Semantic understanding and AI indexing |
The TIL post on AI Index Citability: Structuring Data for LLMs covers the semantic layer. And to understand why Google's indexing changed, the TIL on Google Indexes Differently Now provides the full strategic context.