Open Graph Node.js April 5, 2026

Generate OG Images Dynamically with Node.js (2026)

Create social sharing images on the fly — for blog posts, user profiles, product pages, and dashboards. Four approaches from edge-compatible to pixel-perfect.

Why Dynamic OG Images?

Static OG images mean every page shares the same preview on social media. Dynamic generation creates unique images per page — with the actual title, author, date, and branding. This dramatically improves click-through rates from Twitter/X, LinkedIn, Slack, and Discord link previews.

The standard size is 1200×630 pixels. Twitter also supports a 1200×600 summary card. Always include the og:image meta tag pointing to your generation endpoint or cached image URL.

Method 1: Satori (JSX to SVG)

Satori (by Vercel) converts JSX to SVG — no browser needed. It runs in any JavaScript runtime including edge functions and Cloudflare Workers. The SVG output can be converted to PNG with @resvg/resvg-js.

const satori = require('satori');
const { Resvg } = require('@resvg/resvg-js');
const fs = require('fs');

// Load font (Satori needs explicit font data)
const interFont = fs.readFileSync('./fonts/Inter-Bold.ttf');

async function generateOG(title, subtitle) {
  const svg = await satori(
    {
      type: 'div',
      props: {
        style: { width: 1200, height: 630, display: 'flex', flexDirection: 'column',
          justifyContent: 'center', padding: '60px', background: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)' },
        children: [
          { type: 'h1', props: { style: { color: '#fff', fontSize: 64, margin: 0, lineHeight: 1.2 }, children: title } },
          { type: 'p', props: { style: { color: '#e0d4f7', fontSize: 28, marginTop: 20 }, children: subtitle } }
        ]
      }
    },
    { width: 1200, height: 630, fonts: [{ name: 'Inter', data: interFont, weight: 700 }] }
  );

  const resvg = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } });
  return resvg.render().asPng(); // Buffer
}

Pros: fast (~50ms), no browser, edge-compatible, deterministic output. Cons: limited CSS (no grid, limited flexbox), must load fonts manually, JSX syntax only (no HTML strings).

Method 2: @vercel/og

Vercel's official OG image library wraps Satori with a nicer developer experience. Designed for Next.js API routes and Vercel Edge Functions but works standalone too.

// Next.js App Router: app/api/og/route.tsx
import { ImageResponse } from '@vercel/og';

export const runtime = 'edge';

export async function GET(request) {
  const { searchParams } = new URL(request.url);
  const title = searchParams.get('title') || 'Default Title';

  return new ImageResponse(
    (
      <div style={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column',
        justifyContent: 'center', padding: 60, background: '#0a0a0f' }}>
        <h1 style={{ color: '#fff', fontSize: 60 }}>{title}</h1>
        <p style={{ color: '#818cf8', fontSize: 24 }}>snapapi.pics</p>
      </div>
    ),
    { width: 1200, height: 630 }
  );
}

// Usage: <meta property="og:image" content="/api/og?title=My+Post" />

Pros: simplest DX, auto-deployed on Vercel edge, hot-reload in dev. Cons: Vercel-centric (works elsewhere but needs adaptation), same CSS limitations as Satori.

Method 3: Playwright (Pixel-Perfect)

When you need full CSS — gradients, backdrop filters, custom fonts, complex grid layouts — render an HTML template in Playwright and screenshot it.

const { chromium } = require('playwright');

async function generateOGPlaywright(title, author, date) {
  const browser = await chromium.launch();
  const page = await browser.newPage({ viewport: { width: 1200, height: 630 } });

  const html = `<!DOCTYPE html>
    <html><head><link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
    <style>
      * { margin:0; box-sizing:border-box; }
      body { width:1200px; height:630px; font-family:'Inter',sans-serif;
        background:linear-gradient(135deg,#1e1b4b,#312e81);
        display:flex; flex-direction:column; justify-content:center; padding:80px; }
      h1 { color:#fff; font-size:56px; line-height:1.2; margin-bottom:24px; }
      .meta { color:#a5b4fc; font-size:22px; }
      .logo { position:absolute; bottom:40px; right:60px; color:#6366f1; font-size:28px; font-weight:700; }
    </style></head>
    <body>
      <h1>${title}</h1>
      <div class="meta">By ${author} · ${date}</div>
      <div class="logo">snapapi.pics</div>
    </body></html>`;

  await page.setContent(html, { waitUntil: 'networkidle' });
  await page.evaluateHandle('document.fonts.ready');
  const png = await page.screenshot({ type: 'png' });
  await browser.close();
  return png;
}

Pros: any CSS works, pixel-perfect, real web fonts. Cons: ~300-500ms per image, needs Chromium installed, not edge-compatible.

Method 4: SnapAPI (API-Based)

Skip the infrastructure. Host your OG template as a page (or use any URL), and let SnapAPI capture it as a screenshot.

// Build-time: generate OG images for all blog posts
async function generateAllOGImages(posts) {
  for (const post of posts) {
    const res = await fetch('https://api.snapapi.pics/v1/screenshot', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json', 'X-Api-Key': 'sk_live_your_key' },
      body: JSON.stringify({
        url: `https://mysite.com/og-template?title=${encodeURIComponent(post.title)}&author=${post.author}`,
        viewport: { width: 1200, height: 630 },
        fullPage: false, format: 'png',
        blockAds: true, blockCookieBanners: true
      })
    });
    const png = Buffer.from(await res.arrayBuffer());
    fs.writeFileSync(`./public/og/${post.slug}.png`, png);
    console.log(`Generated OG: ${post.slug}`);
  }
}
# Python — batch generate with async
import httpx, asyncio

async def generate_og(client, title, slug):
    resp = await client.post('https://api.snapapi.pics/v1/screenshot',
        headers={'X-Api-Key': 'sk_live_your_key'},
        json={'url': f'https://mysite.com/og?title={title}',
              'viewport': {'width': 1200, 'height': 630},
              'format': 'png', 'blockAds': True})
    with open(f'og/{slug}.png', 'wb') as f:
        f.write(resp.content)

SnapAPI renders with real Chromium — full CSS support, web fonts, everything. No browser to install or manage. 200 free requests/month.

Method Comparison

MethodSpeedCSS SupportEdge-ReadyBest For
Satori~50msLimited flexboxYesSimple text layouts
@vercel/og~50msLimited flexboxYes (Vercel)Next.js projects
Playwright300-500msFullNoComplex designs
SnapAPI1-3s (network)FullYes (HTTP)Any stack, no infra