Suggestions
← TIL
~3 min read
#astro#seo#og-images#satori

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.

OG Pipeline
TEXT
Satori (JSX → SVG)

Resvg WASM (SVG → PNG)

Edge Function / CDN

Satori draws. Resvg renders.

Satori + Resvg vs. Puppeteer

Satori + Resvg vs. Puppeteer
RecommendedSatori + ResvgPuppeteer
RuntimeLightweight, no heavy dependenciesRequires running Chromium
RenderingSVG → PNG via WASMFull browser
Edge Compatibility✓✓
PerformanceMillisecondsHigher cold starts
CSSPartial supportFull support
Setup ComplexityLowHigh

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

src/pages/og/[slug].png.ts
TS
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.

OG Images vs JSON-LD
LayerGoal
OG ImagesVisual distribution and CTR
JSON-LDSemantic 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.

Link copied to clipboard