Dynamic OG Images That Match Every Page — Generate Them with an API

7 min read Image Generation

The Same OG Image on Every Page

Open your site in a social media debugger. Share a blog post on Twitter. Post a product page to LinkedIn. Chances are they all show the same generic image — your logo on a white background, maybe with your tagline.

Every page on your site has unique content. None of that shows up in the social preview. The title, the topic, the author — all lost behind a single static fallback image.

This matters because OG images are the first thing people see when your link appears in a feed, a Slack channel, or a Discord server. A generic image says “I didn’t bother.” A page-specific image with the actual title says “this content is worth clicking.”

Why Most Sites Skip Dynamic OG Images

The standard approach to dynamic OG images is a headless browser pipeline. You build an HTML template, spin up Puppeteer or Playwright, render the page, take a screenshot, save the image. Some teams use serverless functions with headless Chrome. Others use services like og-image that wrap the same approach in a hosted API.

All of these share the same problems:

  • Slow. Headless Chrome takes 1-3 seconds per image. At scale — hundreds of blog posts, thousands of product pages — that adds up fast.
  • Memory-hungry. Chrome needs 200-500 MB of RAM per instance. Serverless cold starts make it worse.
  • Fragile. CSS rendering in headless Chrome doesn’t always match what you see in a browser. Fonts fail to load. Flexbox layouts shift. You debug visual issues in a headless environment with no inspector.
  • Infrastructure-heavy. You need Chrome binaries, font files, and a server or serverless function to run them on.

The Image Generation API skips all of this. No browser, no HTML, no CSS. You describe the image as a stack of layers — background, shapes, text, images — and the API renders it.

The Template

A standard OG image is 1200x630 pixels. Here’s a template that works for blog posts:

  • Dark background for contrast in social feeds
  • Brand accent color as a thin bar at the top
  • Page title as the main text element
  • Author photo (optional) with smart cropping
  • Site name in smaller text at the bottom

Translated into layers:

import { IterationLayer } from "iterationlayer";
const client = new IterationLayer({ apiKey: "YOUR_API_KEY" });

const generateOgImage = async (title: string, authorPhotoUrl?: string) => {
  const layers = [
    {
      type: "solid-color-background",
      index: 0,
      hex_color: "#1a1a2e",
      opacity: 100,
    },
    {
      type: "rectangle",
      index: 1,
      hex_color: "#e94560",
      position: { x: 0, y: 0 },
      dimensions: { width: 1200, height: 4 },
      opacity: 100,
    },
    {
      type: "text",
      index: 2,
      text: title,
      font_name: "Inter",
      font_weight: "Bold",
      font_size_in_px: 48,
      text_color: "#ffffff",
      text_align: "left",
      position: { x: 60, y: 180 },
      dimensions: { width: 740, height: 300 },
      is_splitting_lines: true,
      opacity: 100,
    },
    {
      type: "text",
      index: 3,
      text: "acme.dev",
      font_name: "Inter",
      font_weight: "Regular",
      font_size_in_px: 20,
      text_color: "#888888",
      text_align: "left",
      position: { x: 60, y: 560 },
      dimensions: { width: 300, height: 40 },
      opacity: 100,
    },
  ];

  if (authorPhotoUrl) {
    layers.push({
      type: "static-image",
      index: 4,
      file: { type: "url", name: "author.jpg", url: authorPhotoUrl },
      position: { x: 880, y: 165 },
      dimensions: { width: 260, height: 260 },
      should_use_smart_cropping: true,
      opacity: 100,
    });
  }

  const result = await client.generateImage({
    dimensions: { width: 1200, height: 630 },
    output_format: "png",
    fonts: [
      {
        name: "Inter",
        weight: "Regular",
        style: "normal",
        file: {
          type: "url",
          name: "Inter-Regular.ttf",
          url: "https://example.com/fonts/Inter-Regular.ttf",
        },
      },
      {
        name: "Inter",
        weight: "Bold",
        style: "normal",
        file: {
          type: "url",
          name: "Inter-Bold.ttf",
          url: "https://example.com/fonts/Inter-Bold.ttf",
        },
      },
    ],
    layers,
  });

  return result;
};

Call generateOgImage("Why We Moved to Postgres") and you get a dark card with a red accent, the title in white, and your site name at the bottom. Add an author photo URL and it appears on the right, smart-cropped to center the face.

What Smart Cropping Does

When you include an author photo — or any image — as a static-image layer with should_use_smart_cropping: true, the API uses AI object detection to find the main subject before fitting the image into the specified dimensions.

For headshots, this means the face stays centered regardless of how the original photo was composed. No need to manually set crop coordinates per author. Upload a full-body shot, a candid, or a formal portrait — the smart crop handles all of them.

Text That Wraps

The is_splitting_lines property on text layers tells the API to auto-wrap text within the bounding box. A short title like “Why Postgres” stays on one line. A long title like “How We Reduced Our Database Query Latency by 80% with Connection Pooling” wraps across multiple lines within the 740px-wide text area.

Text layers also support markdown — **bold** and *italic* render with the corresponding font weight and style. Useful for emphasizing a keyword in the title:

{
  "text": "Why **Postgres** Changed Everything",
  "font_name": "Inter",
  "font_weight": "Regular",
  "font_size_in_px": 48
}

“Postgres” renders in bold. The rest renders in regular weight. Both use the Inter font — the API picks the correct variant automatically.

Build-Time Generation

The most practical approach is generating OG images at build time. Your static site generator already knows every page’s title, description, and metadata. Add a build step that loops over your pages and calls the API for each one:

const pages = [
  { title: "Why We Moved to Postgres", slug: "why-postgres", author: "https://example.com/photos/alice.jpg" },
  { title: "Building a Rate Limiter from Scratch", slug: "rate-limiter", author: "https://example.com/photos/bob.jpg" },
  { title: "Our Approach to Error Handling", slug: "error-handling", author: null },
];

for (const page of pages) {
  const result = await generateOgImage(page.title, page.author ?? undefined);
  const { data: { buffer } } = result;

  await writeFile(`./public/og/${page.slug}.png`, Buffer.from(buffer, "base64"));
}

Then reference the generated images in your page’s meta tags:

<meta property="og:image" content="https://acme.dev/og/why-postgres.png" />

Every page gets a unique, branded OG image. The build adds a few seconds per page — far less than the minutes you’d spend maintaining a headless browser pipeline.

On-Demand Generation

If build-time generation doesn’t fit your workflow — maybe your content is dynamic, or you have too many pages to pre-generate — you can generate on demand and cache the results.

Set up an endpoint that takes a page slug, generates the OG image if it doesn’t exist in your cache, and returns it. The first request takes a moment. Every subsequent request serves the cached version.

The API response is JSON with a base64-encoded image. Decode it and store in S3, R2, or any CDN-backed storage. Point your OG meta tag at the CDN URL.

No Puppeteer, No Chrome, No HTML

The entire pipeline is a JSON request and an image response. No browser binaries to install. No Chrome security patches to track. No Dockerfile with apt-get install chromium. No CSS debugging in a headless environment.

The output is deterministic. Same request, same image. Every time. No font-loading race conditions, no CSS layout shifts, no “it works locally but not in production” surprises.

Get Started

Check the docs for the full layer reference and output format options (PNG, JPEG, WebP, AVIF).

Sign up at iterationlayer.com for a free API key — no credit card required. Start with the template above, swap in your brand colors and fonts, and generate your first batch of OG images.

Start building in minutes

Free trial included. No credit card required.