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 (5)

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.

Collapse
 
apex_stack profile image
Apex Stack

Really appreciate the detail here — the s-maxage=86400 CDN caching strategy is clever. For my use case (8K+ stock ticker pages with OG images), I went the pre-gen route since the tickers are known ahead of time. But your point about on-demand for dynamic content makes a lot of sense.

Satori is interesting — I hadn't considered the React JSX → SVG → PNG pipeline. Does the SVG intermediate step add any noticeable latency compared to going straight to canvas? Curious if you've benchmarked that path.

Thread Thread
 
narender_singh_6c6e271c67 profile image
Narender singh

The SVG step barely matters. Satori does JSX to SVG in about 10-15ms, resvg turns that into PNG in another 20-30ms. Under 50ms total. Raw canvas might be a few ms faster but I never benchmarked them against each other because it wouldn't change the architecture either way.

I picked Satori over canvas because writing layout in canvas coordinates is painful. You end up calculating x/y for every text element, handling word wrap manually, messing with font metrics. It works, but it's a lot of tedious code. Satori lets you write a React component with flexbox and it figures out positioning for you. Much less to maintain.

For your ticker pages, pre-gen at deploy time makes total sense since you already know every page upfront. On-demand is better when you can't predict what pages will exist, like user generated content or search results. And once the CDN caches a response, only the very first request actually generates anything. After that it's just serving a static file.

Thread Thread
 
apex_stack profile image
Apex Stack

Really helpful breakdown on the Satori vs canvas tradeoff — the flexbox layout abstraction is exactly what makes it maintainable at scale. We went the pre-gen route for our 8K+ ticker pages and the build-time cost is negligible since it's just one more step in the Astro build pipeline.

The CDN caching point is key too. Once an OG image is generated and cached, it's essentially free. The only tricky part for us is invalidation — when stock data updates daily, we need the OG images to reflect current prices without regenerating everything. Still figuring out the right balance there.