How to Generate Open Graph Images Dynamically with a Screenshot API

2026-04-20 | Tags: [screenshot-api, og-images, seo, tutorials, use-cases]

When someone shares a URL on Twitter/X, LinkedIn, Slack, or iMessage, the platform fetches the page's Open Graph image and displays it as a preview card. That preview image is the difference between a click and a scroll-past.

Most developers either use static OG images (one image for all pages, which looks generic), rely on design tools (manual, doesn't scale), or reach for image generation libraries (sharp, canvas, Puppeteer — heavy dependencies). A screenshot API offers a fourth path: render an HTML template and screenshot it.

The Core Pattern

The idea: create a simple HTML page that serves as an OG image template, populate it with page-specific data, and capture a 1200×630 screenshot. The screenshot becomes the OG image.

User shares URL →
  Platform fetches page HTML →
  Platform reads <meta property="og:image" content="..."> →
  Platform fetches OG image URL →
  Screenshot API renders template and returns 1200×630 image

The OG image URL points to your screenshot endpoint. When a platform fetches it, it gets a freshly rendered, page-specific image.

Step 1: Create an OG Image Template

A minimal HTML template for an article OG image:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  body {
    width: 1200px;
    height: 630px;
    background: #0f172a;
    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
    display: flex;
    flex-direction: column;
    justify-content: center;
    padding: 80px;
    overflow: hidden;
  }
  .tag {
    color: #60a5fa;
    font-size: 18px;
    font-weight: 600;
    letter-spacing: 0.05em;
    text-transform: uppercase;
    margin-bottom: 24px;
  }
  h1 {
    color: #f8fafc;
    font-size: 56px;
    font-weight: 800;
    line-height: 1.1;
    margin-bottom: 32px;
    max-width: 900px;
  }
  .meta {
    color: #94a3b8;
    font-size: 20px;
  }
  .site {
    position: absolute;
    bottom: 48px;
    right: 80px;
    color: #475569;
    font-size: 18px;
    font-weight: 500;
  }
</style>
</head>
<body>
  <div class="tag">{{CATEGORY}}</div>
  <h1>{{TITLE}}</h1>
  <div class="meta">{{DATE}} · {{READ_TIME}}</div>
  <div class="site">yoursite.com</div>
</body>
</html>

Save this as /pages/og-template.html on your server. It's a static page that renders at 1200×630 — exactly the OG image dimensions.

Step 2: Serve a Template URL with Parameters

Your server needs to serve the template with injected content. A minimal Python example:

from http.server import BaseHTTPRequestHandler
from urllib.parse import urlparse, parse_qs
import urllib.request

class OGTemplateHandler(BaseHTTPRequestHandler):
    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path != '/og-template':
            self.send_response(404)
            self.end_headers()
            return

        params = parse_qs(parsed.query)
        title = params.get('title', ['Untitled'])[0]
        category = params.get('category', ['Article'])[0]
        date = params.get('date', [''])[0]
        read_time = params.get('read_time', ['5 min read'])[0]

        with open('pages/og-template.html', 'r') as f:
            html = f.read()

        # Sanitize and inject (use proper escaping in production)
        html = html.replace('{{TITLE}}', title[:80])
        html = html.replace('{{CATEGORY}}', category)
        html = html.replace('{{DATE}}', date)
        html = html.replace('{{READ_TIME}}', read_time)

        self.send_response(200)
        self.send_header('Content-Type', 'text/html; charset=utf-8')
        self.end_headers()
        self.wfile.write(html.encode())

Your template URL then looks like:

https://yoursite.com/og-template?title=How+to+Scale+PostgreSQL&category=Engineering&date=May+2026&read_time=8+min+read

Step 3: Use the Screenshot API as the OG Image Source

Point your OG image meta tag directly at the screenshot endpoint:

import urllib.parse

def get_og_image_url(title: str, category: str, date: str, read_time: str) -> str:
    template_url = "https://yoursite.com/og-template?" + urllib.parse.urlencode({
        "title": title,
        "category": category,
        "date": date,
        "read_time": read_time
    })

    screenshot_url = "https://hermesforge.dev/api/screenshot?" + urllib.parse.urlencode({
        "url": template_url,
        "width": 1200,
        "height": 630,
        "format": "png",
        "wait_for": "networkidle",
        "key": "YOUR_API_KEY"
    })

    return screenshot_url

In your HTML template for the article:

<meta property="og:image" content="{{ og_image_url }}">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:image" content="{{ og_image_url }}">

When LinkedIn or Twitter fetches your article URL, they follow the og:image URL, which calls the screenshot API, which renders your template with the article's title and metadata, and returns a 1200×630 PNG.

Step 4: Cache the Generated Images

Screenshots are generated on-demand, which adds latency on first share. Cache the result:

import hashlib
import os
import requests

CACHE_DIR = '/var/cache/og-images'
os.makedirs(CACHE_DIR, exist_ok=True)

def get_og_image_cached(title: str, category: str, date: str) -> bytes:
    # Cache key based on content
    cache_key = hashlib.sha256(f"{title}{category}{date}".encode()).hexdigest()
    cache_path = os.path.join(CACHE_DIR, f"{cache_key}.png")

    if os.path.exists(cache_path):
        with open(cache_path, 'rb') as f:
            return f.read()

    # Generate if not cached
    template_url = build_template_url(title, category, date)
    resp = requests.get(
        "https://hermesforge.dev/api/screenshot",
        params={
            "url": template_url,
            "width": 1200,
            "format": "png",
            "wait_for": "networkidle",
            "key": "YOUR_API_KEY"
        },
        timeout=30
    )
    resp.raise_for_status()

    with open(cache_path, 'wb') as f:
        f.write(resp.content)

    return resp.content

Serve the cached PNG directly from your server. The screenshot API call happens once per unique title+category+date combination; subsequent shares serve the cached PNG.

Rate Limit Planning

Site size OG images needed Hermesforge tier
Small blog (< 50 posts) 50 images, one-time Free tier (50/day)
Active blog (50–200 posts) 200 images, then ~5/day new posts Starter ($4, 200/day)
Large site (200+ posts) Bulk pre-generation + daily new Pro ($9, 1,000/day)

With caching, ongoing cost is minimal — one screenshot per new post. The initial bulk generation for existing posts is the main cost.

Pre-generating at Build Time

For static sites (Next.js, Gatsby, Jekyll), generate OG images at build time:

import os
import requests
import json

def prebuild_og_images(posts_file: str, output_dir: str, api_key: str):
    with open(posts_file) as f:
        posts = json.load(f)

    os.makedirs(output_dir, exist_ok=True)

    for post in posts:
        slug = post['slug']
        output_path = os.path.join(output_dir, f"{slug}.png")

        if os.path.exists(output_path):
            print(f"Skipping {slug} (cached)")
            continue

        template_url = build_template_url(
            title=post['title'],
            category=post['category'],
            date=post['date']
        )

        resp = requests.get(
            "https://hermesforge.dev/api/screenshot",
            params={"url": template_url, "width": 1200, "format": "png", "key": api_key},
            timeout=30
        )

        if resp.status_code == 200:
            with open(output_path, 'wb') as f:
                f.write(resp.content)
            print(f"Generated: {slug}.png")
        else:
            print(f"Failed: {slug} — {resp.status_code}")

Run this as part of your build pipeline. OG images are generated once and committed alongside your static assets.


Hermesforge Screenshot API supports 1200×630 captures for OG image generation. Get a free API key — 50 screenshots/day, no signup required.