← All posts

Open Graph Image Generation API: A Production Pattern

September 13, 2026

Why Manually-Designed OG Images Don't Scale

A handful of static OG images works fine for a marketing site with ten pages. It falls apart once you're running a blog, marketplace, docs site, or anything with user-generated content, because every URL needs its own unique, on-brand preview. Nobody has time to open Figma or Canva for the four-hundredth product listing.

The cost of skipping this isn't cosmetic. Links with a custom social preview card consistently pull higher click-through rates than links with a generic placeholder or no image — a blank or broken preview reads as untrustworthy in a crowded feed. Past a few dozen dynamic URLs, a hand-built image library stops being a design task and becomes an infrastructure problem: you need a system that generates a unique OG image automatically for every page, on publish, without a human touching an image editor.

That system has to solve a problem most teams don't anticipate until it bites them: client-side rendering is invisible to the crawlers that fetch these images. Facebook, LinkedIn, and X don't run your JavaScript — they request the URL, read the HTML head, and grab whatever og:image points to. If that image is generated in the browser after page load, the crawler sees nothing.

How Screenshot-API-Based OG Generation Works

There are two fundamentally different ways to generate these images programmatically, and picking the wrong one causes most of the pain teams run into later.

The first is SVG-to-PNG generation, popularized by libraries like satori and widely used in Next.js dynamic OG image setups. You describe a layout in JSX-like syntax, satori converts it to SVG, and a renderer rasterizes it to PNG. It's fast and cheap at the edge, but it's a constrained layout engine — no real CSS cascade, limited flexbox support, no web fonts loaded the way a browser loads them, and no support for complex effects like backdrop blur, gradients over images, or multi-column text wrapping.

The second approach is headless-browser rendering: build an actual HTML/CSS template — the same html to og image approach you'd use for any webpage — with placeholders for title, author, thumbnail, and any other dynamic field, then have a real browser render that page and screenshot it. This is the pattern a screenshot API like Browsevra is built for. Because it's a full layout engine, you get real CSS, real web fonts, real images with lazy-load handling, and pixel-accurate typography — design fidelity that's tedious or impossible to reproduce in an SVG-only pipeline.

The tradeoff is render time and cost per image. Headless Chrome is heavier than an SVG renderer, which is exactly why the caching strategy later in this article matters as much as the template itself.

Building the Request: A Working Pattern

A minimal working setup: an internal endpoint (or a static HTML file served from your own domain) accepts query parameters — title, author, image — and renders an OG image template with those values interpolated into the markup. That URL is never shown to users; it exists purely to be screenshotted.

Your OG image route then calls Browsevra's render endpoint, passing that template URL and a fixed viewport, and returns a PNG. The parameters that matter:

  • Viewport size: set explicitly to 1200x630 so the render matches your target og:image:width/og:image:height rather than getting scaled awkwardly later. For more granular control over device pixel ratio and viewport behavior, see device emulation for headless browsers.
  • Output format: PNG for crisp text and logos, JPEG if you need smaller file sizes for high volumes of cards.
  • Wait conditions: make sure the render waits for web fonts and any images in the template to load before capturing, or you'll bake in flashes of fallback fonts.

The final og:image meta tag simply points to your generation endpoint's URL (ideally the cached version, discussed below), and social platforms fetch it exactly like any other image URL — they don't know or care that it was rendered on demand.

Getting the Dimensions and Meta Tags Right

1200x630 pixels (a 1.91:1 ratio) is the safe universal default across Facebook, LinkedIn, and Slack unfurls. Keep file size under roughly 8MB, though staying well under 1MB is better for fetch speed. Different platforms crop edges slightly differently, so keep your title text, logo, and any critical detail inside a safe center zone rather than flush against the edges.

If X/Twitter is a primary distribution channel, note its preference for a 1200x675 (16:9) crop — a 1200x630 image still displays, but text near the top or bottom edge can get clipped. A current per-platform breakdown, including these edge cases, is maintained in this OG image size cheat sheet.

Ship the full trio of meta tags, not just og:image:






og:image:width and og:image:height let crawlers reserve layout space and skip a slow dimension probe — omitting them is a common, silent cause of delayed or dropped previews.

Caching, Fallbacks, and Crawler Timeouts

Never render on every request. Cache each generated image by slug or a content hash of its inputs, so the same post always resolves to the same stored image URL, and invalidate only when the underlying content changes. Serve that cached image through CDN edge caching with long Cache-Control max-age headers — social crawlers re-fetch aggressively, and you don't want to re-render Chrome on every unfurl.

This isn't just an optimization; it's a reliability requirement. Facebook's crawler times out around five seconds, and a cold, on-request headless render can easily blow past that window, leaving a blank or broken preview cached by the platform for hours. Pre-generating images at publish time — or on first request, then caching aggressively — sidesteps this entirely, a pattern well documented in production write-ups on secure OG image APIs and Next.js OG image patterns.

Always configure a static fallback image for when a render fails — a generic branded card is far better than a 500 error or an empty og:image tag. If your templates pull in remote thumbnails, protect the render endpoint with HMAC-signed URLs so nobody can point your renderer at arbitrary, hostile content, and make sure the endpoint itself is publicly fetchable over HTTPS without auth, since crawlers can't log in.

Common Mistakes That Break Social Previews

Most "og:image not showing" reports trace back to a short list of avoidable errors:

  • Relative image URLs. og:image must be an absolute HTTPS URL — crawlers won't resolve relative paths.
  • Client-side-only generation. If the image tag is injected by JavaScript after load, crawlers never see it.
  • Auth or hotlink protection on the image path, blocking the crawler's anonymous fetch.
  • Wrong Content-Type header — serving a PNG with a text/html header confuses parsers.
  • Stale cache after edits. A social preview not updating usually means the platform cached the old version — clear it with the Facebook Sharing Debugger or LinkedIn Post Inspector after every meaningful change, and check full-page capture edge cases like sticky headers with this full-page screenshot guide if your template includes complex layouts.

Frequently Asked Questions

What's the best image size for an Open Graph card in 2026?

1200x630 pixels remains the universal safe default across Facebook, LinkedIn, and Slack. If X/Twitter is a priority channel, favor 1200x675 to avoid edge cropping, and always declare og:image:width and og:image:height explicitly.

Can I generate OG images at request time instead of at build time?

Yes, but only if you cache aggressively after the first render. Rendering fresh on every crawler request risks exceeding timeout windows (Facebook's is roughly five seconds), so the common pattern is render-once-then-cache by slug or content hash, not render-per-request.

Why does my og:image show up on Facebook but not on X/Twitter?

X reads its own twitter:image tag preferentially and can behave differently with aspect ratios outside 1200x675, so an image sized purely for Facebook's 1.91:1 ratio may get cropped or ignored. Verify both tags independently with each platform's debugger tool.

Do I need a separate twitter:image tag if I already have og:image?

Twitter/X falls back to og:image if no twitter:image is present, so it's not strictly required, but setting twitter:image explicitly gives you control over cropping and lets you serve a 1200x675 variant if needed.

How much does it cost to auto-generate a unique OG image for every page?

Cost scales with render volume, not page count, because cached images aren't re-rendered. A site publishing a few thousand unique cards a month typically pays for renders once per piece of content plus occasional cache invalidations — see Browsevra's pricing for current per-render rates at scale.

Will a headless browser render my custom fonts and CSS correctly for OG images?

Yes — that's the core advantage over SVG-based generators. A real headless Chrome instance loads web fonts, applies your actual CSS cascade, and handles effects like gradients or blur exactly as a browser would, rather than approximating them through a constrained layout engine.

Stop hand-designing preview images one at a time. Point your CMS or app at an HTML template, wire it into a render call, and let the pipeline handle the rest — start with the Browsevra docs to try the render endpoint against your own template in minutes, and check pricing if you're estimating cost across thousands of cards a month. Full details on the platform are at browsevra.