DEV Community

Narender singh
Narender singh

Posted on • Originally published at ogpix-pi.vercel.app

I Built an API That Generates OG Images in 50ms — No Puppeteer Needed

Every website needs Open Graph images for social sharing. But generating them is a pain:

  • Puppeteer/Playwright: Spin up a headless browser, render HTML, screenshot it. Slow (~2-5 seconds), heavy (200MB+ Chrome binary), expensive to host.
  • Canvas libraries: Write imperative drawing code. No hot reload, no component reuse, painful text layout.
  • Manual design: Open Figma for each page. Doesn't scale past 10 pages.

I wanted something simpler. So I built OGPix — an API that generates beautiful OG images from URL parameters in ~50ms.

How It Works

Your og:image meta tag becomes a URL:

<meta property="og:image"
  content="https://ogpix-pi.vercel.app/api/og?title=My+Article&theme=dark&key=YOUR_KEY" />
Enter fullscreen mode Exit fullscreen mode

That's it. When anyone shares your link on Twitter, LinkedIn, Slack, or Discord — they see a beautiful preview image generated on the fly.

10 Themes

  • Gradient — Bold purple gradient
  • Minimal — Clean white background
  • Dark — Sleek dark mode
  • Sunset — Warm orange tones
  • Ocean — Deep blue
  • Forest — Nature green
  • Mono — High contrast black & white
  • Branded — Your custom colors

  • Neon — Cyberpunk neon glow

  • Warm — Cozy cream & terracotta

Try them all in the interactive playground.

The Tech

  • Satori (not Puppeteer) renders React components to SVG, then to PNG
  • Runs on Vercel Edge Functions — no cold starts, globally distributed
  • Images are CDN cached — same parameters = instant response
  • Built with Next.js + Supabase + Stripe

Next.js Example

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);

  const ogUrl = new URL("https://ogpix-pi.vercel.app/api/og");
  ogUrl.searchParams.set("title", post.title);
  ogUrl.searchParams.set("description", post.excerpt);
  ogUrl.searchParams.set("theme", "dark");
  ogUrl.searchParams.set("key", process.env.OGPIX_KEY);

  return {
    openGraph: {
      images: [{ url: ogUrl.toString(), width: 1200, height: 630 }],
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

Works With Everything

It's just a URL, so it works with any framework:

  • Next.jsgenerateMetadata
  • Astro — frontmatter + layout
  • Hugo — partial template
  • WordPresswp_head hook
  • Plain HTML<meta> tag

Pricing

  • Free: 100 images/month (with watermark)
  • Starter: $9/mo — 5,000 images, no watermark
  • Pro: $29/mo — 50,000 images

Most personal sites never leave the free tier.

Try It

Would love feedback on what themes or features you'd want. What does your ideal OG image look like?

Top comments (2)

Collapse
 
apex_stack profile image
Apex Stack

The Puppeteer approach breaks down fast once you need OG images at any real scale — cold start alone can add 2-3 seconds on a serverless function, and the memory footprint makes horizontal scaling expensive.

I had to solve a version of this for a financial data site with 8,000+ stock and ETF pages, each needing its own branded OG image with ticker, company name, and a price sparkline. Puppeteer was the obvious first instinct but the idea of spinning up a headless browser per request was a non-starter. We ended up pre-generating images at deploy time using a Node.js + canvas pipeline and serving them as static assets from CDN — solves the latency problem entirely but means you're regenerating on every deploy rather than on-demand.

The on-demand API approach you've built is the right answer for dynamic content where you can't pre-generate. Curious about your caching layer — are you storing generated images at the CDN edge keyed by parameters, or regenerating on every request? For something serving social sharing previews, a long-lived edge cache probably gets your effective p99 well below 50ms for any URL that's been shared before.

Collapse
 
narender_singh_6c6e271c67 profile image
Narender singh

Great question! Yes, images are CDN cached at the edge keyed by the full query string. Same params = instant cache hit (~5ms). TTL is 24h via s-maxage=86400.

Your pre-gen approach is smart for 8K+ known pages. On-demand API shines for dynamic content you can't predict at build time.

I chose Satori over canvas for more consistent text rendering across environments — React JSX → SVG → PNG, no browser needed.