Guide · 2026-10-10

Dynamic Open Graph images in Next.js

Every page gets its own share image, rendered the first time a crawler asks for it. No image route, no build step, one small function.

The idea

Next.js can generate OG images itself with ImageResponse, but that means a Satori-rendered subset of CSS, cold starts on your own function, and no caching between deploys. A signed URL moves the work to imageapis.com: your page emits a URL, the crawler fetches it, the first fetch renders in a real browser and every later fetch is served from the edge cache.

1. Create a signed base

Once, with your key:

curl -X POST https://api.imageapis.com/v1/signed-bases \
  -H 'X-API-Key: YOUR_KEY' -H 'Content-Type: application/json' \
  -d '{"name":"blog cards","template":"og"}'

Keep the id (sb_…) and the secret (sbs_…) in your environment as OG_BASE and OG_SECRET. Locking the base to the og template means a leaked secret can only ever render that card.

2. Sign in generateMetadata

// app/blog/[slug]/page.tsx
import { createHmac } from "node:crypto";

function ogUrl(params: Record<string, string>) {
  const path = `/s/${process.env.OG_BASE}/og.png`;
  const query = new URLSearchParams(params).toString();
  const sig = createHmac("sha256", process.env.OG_SECRET!).update(`${path}?${query}`).digest("hex");
  return `https://img.imageapis.com${path}?${query}&sig=${sig}`;
}

export async function generateMetadata({ params }) {
  const post = await getPost(params.slug);
  const image = ogUrl({ title: post.title, description: post.excerpt, site: "example.com", tag: "Blog", theme: "dark" });
  return {
    title: post.title,
    openGraph: { images: [{ url: image, width: 1200, height: 630 }] },
    twitter: { card: "summary_large_image", images: [image] },
  };
}

URLSearchParams encodes spaces as + and the signature covers exactly the string you send, so build the URL from the same query you signed.

3. Check it

Paste a page URL into the X card validator or LinkedIn's Post Inspector. The first check renders the card (one image against your plan); the card is then cached for a week at the edge and re-rendered on demand after that.

Your own design

Save a layer template with {{title}} and {{author}} placeholders, pass its id instead of og in the path, and the query parameters become the variables. Change the template and the next fetch of every URL picks it up.

Get a free key: 150 images a month API reference