DEV Community

Hugo Bernardo Cardoso
Hugo Bernardo Cardoso

Posted on

From Zero to Production: Building a Secure OG Image API

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:

  1. A signed URL layer — every request carries an HMAC-SHA256 signature so only authorized callers can trigger renders
  2. A render service — headless Chrome (via Browsershot) renders Svelte 5 templates at exactly 1200×630
  3. 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
Enter fullscreen mode Exit fullscreen mode

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')}`;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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:

  1. Sign every request — HMAC-SHA256 in the URL, not headers
  2. Separate rendering from serving — a crash in Chrome shouldn't take down your API
  3. Cache aggressively — immutable cache headers, long max-age
  4. Deduct credits only on success — atomic locks, machine-readable errors
  5. 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)