From Zero to Production: Building a Secure OG Image API
Every time you paste a link into Slack, WhatsApp, or X, a tiny war is being fought. The social platform's crawler hits your page, reads the Open Graph tags, and decides what to show. If your og:image is missing, slow, or ugly, your link becomes a bare URL. No thumbnail. No headline. No click.
For most developers, the fix is a static image. But static images don't scale when you're generating a unique preview for every product page, blog post, or pricing tier. You need dynamic OG images — and you need them served securely, at speed, without melting your server budget.
This is the story of building exactly that: a production-grade OG image API with HMAC-signed URLs, headless Chrome rendering, and edge caching that makes the first request the only expensive one.
The Problem: Why Your Previews Fail
Client-side OG image generation is a trap. Social network crawlers — LinkedIn, Twitter, Facebook — don't execute JavaScript. They fetch your URL, read the raw HTML, and move on. If your og:image is generated client-side, the crawler sees nothing.
Server-side generation fixes this, but introduces its own problems. Rendering a 1200×630 image on every request is expensive. A naive implementation using a headless browser per request will eat CPU, exhaust memory, and time out under load.
The solution is a dedicated API that separates rendering from serving. You pay for the render once, cache the result aggressively, and let the edge handle the rest.
The Architecture: Three Moving Parts
FastOG's approach breaks down into three components that work together:
- A signed URL layer — every request carries an HMAC-SHA256 signature so only authorized callers can trigger renders
- A render service — headless Chrome (via Browsershot) renders Svelte 5 templates at exactly 1200×630
-
A caching layer — successful renders are cached for a year with
Cache-Control: public, max-age=31536000, immutable
The signing layer is the part most people get wrong. You might be tempted to put your API key in a header. That works for your own server-side code, but it breaks the moment a social network crawler tries to fetch the image. Crawlers don't send custom headers. They just fetch the URL.
That's why signatures live in the URL itself.
URL-Signed Security: The HMAC Pattern
Here's the core endpoint:
GET /api/v1/og?key=YOUR_API_KEY&s=SIGNATURE&title=Hello&template=blog
The signature is computed by taking the canonical sorted query string, RFC 3986-encoding it, and HMAC-ing it with your secret key. Every API key has its own secret.
A minimal Node.js signer looks like this:
import crypto from 'crypto';
function sign(params, secret) {
const canonical = Object.keys(params)
.filter(k => k !== 's')
.sort()
.map(k => `${k}=${encodeURIComponent(params[k])}`)
.join('&');
const signature = crypto
.createHmac('sha256', secret)
.update(canonical)
.digest('hex');
return `${canonical}&s=${signature}`;
}
const params = {
key: 'your_api_key',
title: 'How to Build an OG Image API',
template: 'devblog'
};
const url = `https://fastog.com/api/v1/og?${sign(params, 'your_secret')}`;
The signature goes in the URL because that's the only thing crawlers will faithfully transmit. It's a deliberate trade-off: you accept that signatures are visible in logs in exchange for images that work everywhere.
Rendering: Why Satori Isn't Enough
You can generate OG images without a browser. Libraries like Satori render JSX to SVG, then you convert to PNG. It's fast and lightweight. But it has limits: no complex CSS, no web fonts, no JavaScript.
When you need 41 different templates — ecommerce cards with prices and ratings, podcast episodes with progress bars, countdown timers for launches — a real browser becomes the pragmatic choice.
FastOG runs a separate Node render service that receives render jobs and drives headless Chrome via Browsershot. The service has a self-healing watchdog: a health check endpoint that auto-restarts the Docker container if it wedges. This matters because headless Chrome will wedge. It's not a question of if, but when.
The render service is separate from the API for a reason. If Chrome crashes, the API stays up. If the API is under load, renders queue gracefully. You don't want a browser crash taking down your whole endpoint.
Credit Economics: Deduct Only on Success
The billing model is simple: each render costs 1 credit. But the important detail is when credits are deducted.
Deduction happens only after a successful render, protected by an atomic lock to prevent double-spending. If the render fails — invalid parameters, render service unreachable, rate limit exceeded — you get a machine-readable error code (insufficient_credits, render_unreachable) and your credits stay untouched.
The response headers tell you where you stand:
X-Renders-Remaining: 42
X-FastOG-Watermark: false
This matters for production systems. You can build retry logic around 502 and 503 responses without worrying about being charged for failed attempts.
Caching: The First Request Is the Only Expensive One
Here's the economics that make this viable: the first request for a given set of parameters triggers a render. That render costs 1 credit. Every subsequent request for the same URL — from LinkedIn's crawler, from Slack's unfurler, from a user's browser — hits the cache.
The Cache-Control: public, max-age=31536000, immutable header tells every intermediary (CDN, browser, crawler) to hold onto the image for a year. Your cost per image approaches zero as the image gets shared more.
This is the pattern that makes dynamic OG images practical at scale. You're not paying per impression; you're paying per unique image.
From Zero to Production: The Checklist
If you're building your own OG image service, here's the minimum viable architecture:
- Sign every request — HMAC-SHA256 in the URL, not headers
- Separate rendering from serving — a crash in Chrome shouldn't take down your API
- Cache aggressively — immutable cache headers, long max-age
- Deduct credits only on success — atomic locks, machine-readable errors
- Add a health check — headless Chrome will crash; plan for it
FastOG implements all of this out of the box. You get 100 free credits on signup, credit packs start at $2, and there's a free OG Image Tester that requires no account at all — you can preview templates and download a real PNG client-side before committing to anything.
The free tester is the fastest way to understand what you're buying. Paste your title, pick a template, see exactly what LinkedIn will display. No signup, no credit card, no friction.
The Bottom Line
Dynamic OG images are a competitive advantage. A link with a well-designed preview gets clicked; a bare URL gets scrolled past. The technical challenge is making generation secure, reliable, and affordable.
URL-signed HMAC authentication solves the crawler problem. A dedicated render service with a watchdog solves the reliability problem. Year-long immutable caching solves the cost problem.
The result is an API where the first request costs one credit and everything after is free — and that's a trade worth making.
Top comments (0)