Automate OG Image Generation with a Screenshot API
Every time someone shares your link on Twitter, Slack, Discord, or WhatsApp, those platforms fetch your Open Graph image. A compelling preview image dramatically increases click-through rates. But designing unique OG images for every page is tedious — especially for dynamic content like blog posts, user profiles, or dashboards.
What if you could generate them automatically?
The Idea
Instead of designing static images, create an HTML template for your OG images, host it at a predictable URL, and use a screenshot API to capture it as a PNG. The result: dynamic, always-current social preview images with zero manual design work.
Step 1: Create an OG Image Template
Build a simple HTML page that accepts query parameters and renders a styled card:
<!-- og-template.html -->
<!DOCTYPE html>
<html>
<head>
<style>
body {
margin: 0;
width: 1200px;
height: 630px;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
color: white;
}
.card {
text-align: center;
padding: 60px;
max-width: 900px;
}
h1 { font-size: 56px; margin: 0 0 20px; line-height: 1.2; }
p { font-size: 24px; opacity: 0.85; margin: 0; }
.logo { font-size: 18px; margin-top: 40px; opacity: 0.6; }
</style>
</head>
<body>
<div class="card">
<h1 id="title">Your Title Here</h1>
<p id="subtitle">Your subtitle or description</p>
<div class="logo">yoursite.com</div>
</div>
<script>
const params = new URLSearchParams(window.location.search);
if (params.get('title')) document.getElementById('title').textContent = params.get('title');
if (params.get('subtitle')) document.getElementById('subtitle').textContent = params.get('subtitle');
</script>
</body>
</html>
Host this template on your server. Now any page's OG image can be generated by passing parameters.
Step 2: Capture with the Screenshot API
Use the free screenshot API to capture your template at exactly 1200x630 (the standard OG image size):
curl "https://hermesforge.dev/api/screenshot?url=https://yoursite.com/og-template.html?title=My%20Blog%20Post&width=1200&height=630&format=png" \
-o og-image.png
Python
import requests
import urllib.parse
def generate_og_image(title, subtitle=""):
template_url = f"https://yoursite.com/og-template.html?title={urllib.parse.quote(title)}&subtitle={urllib.parse.quote(subtitle)}"
response = requests.get(
"https://hermesforge.dev/api/screenshot",
params={
"url": template_url,
"width": 1200,
"height": 630,
"format": "png"
}
)
return response.content
# Generate for a blog post
image = generate_og_image(
"How to Build a REST API in 2026",
"A practical guide with Python and FastAPI"
)
with open("og-image.png", "wb") as f:
f.write(image)
Node.js
const https = require('https');
const fs = require('fs');
function generateOGImage(title, subtitle = '') {
const templateUrl = `https://yoursite.com/og-template.html?title=${encodeURIComponent(title)}&subtitle=${encodeURIComponent(subtitle)}`;
const apiUrl = `https://hermesforge.dev/api/screenshot?url=${encodeURIComponent(templateUrl)}&width=1200&height=630&format=png`;
return new Promise((resolve, reject) => {
https.get(apiUrl, (res) => {
const chunks = [];
res.on('data', chunk => chunks.push(chunk));
res.on('end', () => resolve(Buffer.concat(chunks)));
res.on('error', reject);
});
});
}
// Usage
generateOGImage('My Amazing Post', 'Published March 2026')
.then(buffer => fs.writeFileSync('og-image.png', buffer));
Step 3: Set Up Dynamic Meta Tags
In your site's HTML, point the og:image tag at your screenshot API:
<meta property="og:image" content="https://hermesforge.dev/api/screenshot?url=https://yoursite.com/og-template.html?title=Your%20Post%20Title&width=1200&height=630&format=png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />
When social platforms crawl your page, they'll hit the screenshot API, which captures your template with the right title, and returns a fresh PNG.
Step 4: Add Caching for Performance
For production use, cache generated images instead of generating them on every social platform crawl:
import hashlib
import os
import requests
import urllib.parse
CACHE_DIR = "./og-cache"
os.makedirs(CACHE_DIR, exist_ok=True)
def get_og_image(title, subtitle=""):
# Create cache key from content
cache_key = hashlib.md5(f"{title}:{subtitle}".encode()).hexdigest()
cache_path = os.path.join(CACHE_DIR, f"{cache_key}.png")
# Return cached version if exists
if os.path.exists(cache_path):
with open(cache_path, "rb") as f:
return f.read()
# Generate fresh image
template_url = f"https://yoursite.com/og-template.html?title={urllib.parse.quote(title)}&subtitle={urllib.parse.quote(subtitle)}"
response = requests.get(
"https://hermesforge.dev/api/screenshot",
params={
"url": template_url,
"width": 1200,
"height": 630,
"format": "png"
}
)
# Cache the result
with open(cache_path, "wb") as f:
f.write(response.content)
return response.content
Advanced: Dark Mode Variants
Generate both light and dark variants for platforms that support them:
# Light mode (default)
curl "https://hermesforge.dev/api/screenshot?url=https://yoursite.com/og-template.html?title=My%20Post&width=1200&height=630" -o og-light.png
# Dark mode
curl "https://hermesforge.dev/api/screenshot?url=https://yoursite.com/og-template.html?title=My%20Post&dark_mode=true&width=1200&height=630" -o og-dark.png
Why This Approach Works
- No design tools needed — your OG images are HTML+CSS, which you already know
- Always current — change the template once, all images update automatically
- Dynamic content — user profiles, dashboards, and live data get unique previews
- Free at low volumes — the screenshot API's free tier handles personal sites and small projects
- Standard sizes — 1200x630 works across Twitter, Facebook, LinkedIn, Slack, Discord, and WhatsApp
Real-World Examples
This pattern is used by Vercel's og library, GitHub's social previews, and Dev.to's article cards. The difference: instead of running your own headless browser infrastructure, you offload it to an API call.
Every link your users share becomes a branded, informative preview — automatically.