Dynamic OG images without a build step

A build script that runs once per blog post cannot make a card for a user, a signup or an order that does not exist yet. Here is the version of og:image that can.

The build-time approach and its ceiling

The common pattern for OG images is a script that runs before next build or inside a static site generator's build step: loop over every page you know about, render its card, write it to public/og/. It works well for a blog or a docs site, because the set of pages is known in advance and does not change between deploys.

It stops working the moment the image depends on something that only exists at request time: a user who just signed up, a share card for content someone just posted, a certificate issued after a course finished, a ticket for an order placed five seconds ago. There is no build to run those images at, because there is no list of them until they happen.

Dynamic images with a signed URL

The alternative is to make the image URL itself do the work. A signed URL encodes a template id, your data as query variables, and an HMAC signature under a secret only your account knows. Point og:image at it and:

There is no build step, no queue, no server process of yours that has to run on every pageview. Your backend's only job is string concatenation: build the URL with the right variables and sign it, the same way you would build any other link.

Worked example: a signup welcome card

Say a new user signs up and you want their signup announcement, shared to Slack or Discord, to unfurl with a personal card instead of your generic homepage image.

1. Save the template once, with the two things that change as variables:

POST /v1/templates201 application/json
curl -X POST https://ogrender.dev/v1/templates \
  -H "Authorization: Bearer sk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "signup-welcome",
    "html": "<div style=\"display:flex;flex-direction:column;justify-content:center;height:100%;
             padding:80px;font-family:Inter;background:#0a0a0a;color:#fff\">
             <div style=\"font-size:22px;color:#888\">Welcome to Acme</div>
             <div style=\"font-size:56px;font-weight:700;margin-top:12px\">{{name}}</div>
             <div style=\"font-size:22px;color:#888;margin-top:8px\">Member #{{number}}</div>
             </div>"
  }'

2. On every signup, your backend builds and signs one URL (Node shown, Python and PHP in the signing docs):

nodesigns GET /v1/img/:id.:ext
import { createHmac } from "node:crypto";

function welcomeCardUrl(user) {
  const variables = { name: user.displayName, number: String(user.id) };
  const enc = (s) => encodeURIComponent(s)
    .replace(/[!'()*]/g, (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`);
  const query = Object.keys(variables).sort()
    .map((k) => `${enc(k)}=${enc(variables[k])}`)
    .join("&");
  const sig = createHmac("sha256", process.env.OGRENDER_SIGNING_SECRET)
    .update(`tpl_signup_welcome\npng\n${query}`)
    .digest("hex");
  return `https://ogrender.dev/v1/img/tpl_signup_welcome.png?${query}&sig=${sig}`;
}

3. Drop it straight into the page that announces the new member, no image file ever written by your app:

og:image tagno server call
<meta property="og:image" content="https://ogrender.dev/v1/img/tpl_signup_welcome.png?name=Ada%20Lovelace&number=482&sig=...">

The first unfurl (Slack fetching the link preview, usually within seconds of the message being sent) renders the card. Every unfurl after that, on any platform, for the next 24 hours, is a cache hit: free, and fast enough that a chat client's link-preview timeout never sees a slow response.

What this does not do

A signed URL is still a URL, so it goes wherever a URL goes: an og:image tag, a chat message, an email, a Discord bot's embed. It does not fetch a remote profile photo into the card yet (v2.1); until then, inline an avatar as a data: URI in the request that calls /v1/render directly if you need one, or leave the card to typography and color, which is what the example above does.

Full request and response shapes, the signing rule, and code in Node, Python and PHP are in templates and signed URLs. Try any HTML free, no signup, in the playground.