How to Build an Open Graph Image Generator with a Screenshot API

Build a system that generates custom social media preview images automatically. Every page on your site gets a unique, dynamic og:image — no design tool required.

When someone shares a link on Twitter, LinkedIn, Slack, or Discord, those platforms look for an Open Graph image — the og:image meta tag — and display it as a visual preview. A good preview image dramatically increases click-through rates. A missing or broken one makes your link look like spam.

The problem: creating unique preview images for every page on your site is tedious if done manually in Figma or Photoshop. The solution: generate them programmatically using a screenshot API.

The Architecture

The approach is straightforward:

Step 1: Create an HTML template for your preview card (your brand, title, description, any dynamic data).

Step 2: For each page, render that template with page-specific content.

Step 3: Screenshot the rendered template at 1200x630 pixels (the standard OG image size).

Step 4: Serve or cache the resulting image and reference it in your og:image meta tag.

Step 1: Build the OG Template

Create a simple HTML page that looks like your desired preview card. This will be the template that gets screenshotted:

<!DOCTYPE html>
<html>
<head>
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    width: 1200px;
    height: 630px;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 80px;
    background: linear-gradient(135deg, #0a0a0a, #1a1a2e);
    color: #fff;
    font-family: -apple-system, sans-serif;
  }
  .tag {
    font-size: 18px;
    text-transform: uppercase;
    letter-spacing: 0.1em;
    opacity: 0.6;
    margin-bottom: 24px;
  }
  h1 {
    font-size: 56px;
    font-weight: 700;
    line-height: 1.2;
    margin-bottom: 24px;
  }
  .domain {
    font-size: 20px;
    opacity: 0.5;
    margin-top: auto;
  }
</style>
</head>
<body>
  <div class="tag">Blog Post</div>
  <h1>{{TITLE}}</h1>
  <div class="domain">yoursite.com</div>
</body>
</html>

Step 2: Generate Images Dynamically

Now build an endpoint in your app that fills in the template and screenshots it:

const express = require('express');
const fetch = require('node-fetch');
const app = express();

app.get('/og-image', async (req, res) => {
  const title = req.query.title || 'Default Title';
  const tag = req.query.tag || 'Article';

  // Build the HTML with dynamic content
  const html = \`
    <!DOCTYPE html>
    <html>
    <head>
      <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body {
          width: 1200px; height: 630px;
          display: flex; flex-direction: column;
          justify-content: center; padding: 80px;
          background: linear-gradient(135deg, #0a0a0a, #1a1a2e);
          color: #fff; font-family: -apple-system, sans-serif;
        }
        .tag { font-size: 18px; text-transform: uppercase;
               letter-spacing: 0.1em; opacity: 0.6;
               margin-bottom: 24px; }
        h1 { font-size: 56px; font-weight: 700;
             line-height: 1.2; margin-bottom: 24px; }
        .domain { font-size: 20px; opacity: 0.5;
                  margin-top: auto; }
      </style>
    </head>
    <body>
      <div class="tag">\${tag}</div>
      <h1>\${title}</h1>
      <div class="domain">yoursite.com</div>
    </body>
    </html>
  \`;

  // Screenshot the HTML using a PDF/Screenshot API
  const response = await fetch(
    'https://snapapi-production-4f80.up.railway.app/api/screenshot',
    {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.SNAPAPI_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        html: html,
        width: 1200,
        height: 630,
        format: 'png'
      })
    }
  );

  const image = await response.buffer();
  res.set('Content-Type', 'image/png');
  res.set('Cache-Control', 'public, max-age=86400');
  res.send(image);
});

Step 3: Reference in Your Meta Tags

In each page's <head>, point the OG image to your generation endpoint:

<meta property="og:image"
  content="https://yoursite.com/og-image?title=How+to+Build+Something&tag=Tutorial">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">

Performance Optimization: Caching

You don't want to generate a screenshot on every social media crawl. Cache aggressively:

Option A: Generate at build time. During your site's build step (Next.js, Gatsby, Hugo), generate all OG images and save them as static files. Zero runtime cost.

Option B: Generate on first request, then cache. Use a CDN with cache headers. The first request generates the image; subsequent requests serve from cache. The Cache-Control: public, max-age=86400 header in the code above does this.

Option C: Pre-generate when content changes. When a blog post is published or updated, trigger OG image generation as part of the publish workflow.

Tip: The standard OG image dimensions are 1200×630 pixels. Twitter Cards use 1200×628 (close enough that 1200×630 works for both). Always specify width and height in your meta tags — platforms render faster when they know dimensions upfront.

Going Further: Dynamic Data in Preview Cards

The template approach becomes very powerful once you start pulling in real data. Your OG images can include author avatars from your database, view counts or read times from your CMS, dynamic charts or metrics for dashboard shares, or product images and prices for e-commerce pages. The template is just HTML — anything you can render in a browser, you can put in a preview card.

Start Generating OG Images

SnapAPI turns any HTML into a screenshot. Perfect for dynamic Open Graph images. Try it live.

Try Interactive Demo

Common Pitfalls

Don't use external fonts without hosting them. If your template references a Google Font, the screenshot might render before the font loads. Either inline the font as base64 or use system fonts for reliability.

Don't forget the fallback. If image generation fails, you should have a static default OG image so the share preview isn't empty.

Test with real platforms. Use Facebook's Sharing Debugger and Twitter's Card Validator to verify your images render correctly. These tools also force a cache refresh, useful when updating images.

Keep text large. Preview images are often displayed small (300-500px wide). Text smaller than 40px in the template will be unreadable when scaled down. When in doubt, make it bigger.

Kevin, Founder of SnapAPI
Kevin
Founder, SnapAPI

Disclosure: This article was written with the help of Claude, an AI assistant by Anthropic.