Automate Social Media OG Images with a Screenshot API
Automate Social Media OG Images with a Screenshot API
Every time someone shares your link on Twitter, LinkedIn, or Facebook, the platform looks for an og:image meta tag. No image? Your link gets a plain text card that nobody clicks. A good OG image can double your click-through rate — but creating one for every page is tedious.
Here's a better approach: automatically screenshot your own pages at OG dimensions and serve the result as your og:image.
The Concept
Instead of designing OG images in Figma for every blog post, product page, or landing page:
- Design your page to look good at 1200x630 (the OG standard)
- Screenshot it at those exact dimensions via API
- Serve the screenshot URL as your
og:image
The page is the image. Any time you update the page, the image updates too.
Quick Implementation
Static Sites (Build Time)
Generate OG images during your build step:
import requests
import os
API = "https://hermesforge.dev/api/screenshot"
SITE = "https://yoursite.com"
OUTPUT_DIR = "public/og"
os.makedirs(OUTPUT_DIR, exist_ok=True)
pages = [
"/",
"/about",
"/blog/my-first-post",
"/pricing",
]
for page in pages:
url = f"{SITE}{page}"
params = {
"url": url,
"viewport": "og", # 1200x630
"format": "png",
"block_ads": "true",
"delay": "2000", # Wait for fonts/images
}
resp = requests.get(API, params=params)
resp.raise_for_status()
slug = page.strip("/").replace("/", "-") or "index"
path = f"{OUTPUT_DIR}/{slug}.png"
with open(path, "wb") as f:
f.write(resp.content)
print(f"Generated OG image: {path} ({len(resp.content) // 1024}KB)")
Then in your HTML templates:
<meta property="og:image" content="https://yoursite.com/og/my-first-post.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
Dynamic Sites (On Demand)
For sites with many pages, generate OG images on the fly:
// Express middleware
app.get('/og-image/*', async (req, res) => {
const pagePath = req.params[0] || '';
const pageUrl = `https://yoursite.com/${pagePath}`;
const apiUrl = new URL('https://hermesforge.dev/api/screenshot');
apiUrl.searchParams.set('url', pageUrl);
apiUrl.searchParams.set('viewport', 'og');
apiUrl.searchParams.set('format', 'png');
apiUrl.searchParams.set('block_ads', 'true');
const response = await fetch(apiUrl);
if (!response.ok) {
return res.status(502).send('OG image generation failed');
}
res.set('Content-Type', 'image/png');
res.set('Cache-Control', 'public, max-age=86400'); // Cache 24h
const buffer = await response.arrayBuffer();
res.send(Buffer.from(buffer));
});
Then reference it dynamically:
<meta property="og:image" content="https://yoursite.com/og-image/blog/my-post">
Platform-Specific Images
Different platforms have different optimal dimensions. Use viewport presets to generate each:
platforms = {
"og": "og", # 1200x630 — Facebook, general
"twitter": "twitter", # 1200x675 — Twitter/X cards
"linkedin": "linkedin", # 1200x627 — LinkedIn posts
}
for platform, viewport in platforms.items():
params = {
"url": page_url,
"viewport": viewport,
"format": "png",
"block_ads": "true",
}
resp = requests.get(API, params=params)
with open(f"og/{slug}-{platform}.png", "wb") as f:
f.write(resp.content)
Then use platform-specific meta tags:
<!-- Facebook / Default -->
<meta property="og:image" content="https://yoursite.com/og/post-og.png">
<!-- Twitter -->
<meta name="twitter:image" content="https://yoursite.com/og/post-twitter.png">
<meta name="twitter:card" content="summary_large_image">
Crop the Best Part with Clip
Sometimes the full viewport isn't ideal for OG. Use the clip parameter to capture just the hero section:
# Capture only the top 630px of a 1200px-wide page
curl "https://hermesforge.dev/api/screenshot?url=https://yoursite.com&width=1200&clip=0,0,1200,630&format=png" \
-o og-image.png
This is perfect when your page has a strong hero section but the rest doesn't look good at OG dimensions.
CI/CD Integration
Regenerate OG images on every deploy:
# GitHub Actions
name: Generate OG Images
on:
push:
branches: [main]
paths: ['content/**', 'src/pages/**']
jobs:
og-images:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install requests
- run: python scripts/generate_og_images.py
- uses: actions/upload-artifact@v4
with:
name: og-images
path: public/og/
Tips
- Add
delay=2000— Fonts, images, and CSS animations need time to load. 2 seconds is usually enough. - Use
block_ads=true— Cookie banners and ad overlays ruin OG images. - Cache aggressively — OG images don't change often. Cache for 24 hours minimum.
- Use PNG, not WebP — Social platforms handle PNG more reliably than WebP for og:image.
- Design for 1200x630 — Make sure your page's above-the-fold content looks good at exactly these dimensions. Preview with
viewport=og. - Test with social debuggers — Facebook Sharing Debugger, Twitter Card Validator, and LinkedIn Post Inspector all let you preview how your link will look.
Why Not Use a Template?
Template-based OG image generators (like @vercel/og) work well for simple text+logo images. But they can't capture:
- Interactive charts and data visualizations
- Real page layouts with actual CSS styling
- Dynamic content that changes frequently
- Complex components that are hard to replicate in a template
Screenshot-based OG images show the actual page. What you see is what gets shared.