How to Automate Social Media Preview Screenshots with Python
If you've ever built a link preview feature, monitored social share cards, or needed to audit Open Graph metadata across hundreds of URLs, you've probably reached for a screenshot API at some point.
Here's a practical guide to doing it at scale with Python.
The Use Case
Social platforms generate preview cards from Open Graph tags — the og:image, og:title, og:description metadata on a page. When a link is shared on LinkedIn, Twitter, Slack, or iMessage, the platform renders that metadata into a card.
The problem: you can't verify what that card looks like by reading the HTML. You need to see it rendered.
Common reasons you'd want to automate this:
- Content QA: verify that all your articles, products, or campaigns have correct preview images before publishing
- Competitor monitoring: track how competitors' landing pages appear when shared
- Link preview features: build a URL unfurler that shows a real screenshot alongside extracted metadata
- SEO auditing: catch pages with missing or broken og:image references
Basic Setup
You'll need Python 3.8+ and the requests library.
import requests
import time
from pathlib import Path
API_BASE = "https://hermesforge.dev/api"
API_KEY = "your-api-key-here" # get one free at hermesforge.dev
def screenshot_url(url: str, width: int = 1200, height: int = 630) -> bytes:
"""
Capture a screenshot at Open Graph dimensions (1200x630 is standard).
Returns raw image bytes (WebP format).
"""
response = requests.get(
f"{API_BASE}/screenshot",
params={
"url": url,
"width": width,
"height": height,
"format": "webp",
"full_page": "false", # viewport only, not full scroll
},
headers={"X-API-Key": API_KEY},
timeout=30,
)
response.raise_for_status()
return response.content
The 1200x630 dimensions match the standard Open Graph image size, so the screenshot represents roughly what platforms will render as a preview card.
Batch Processing with Rate Limit Handling
For bulk auditing, you'll want to handle rate limits gracefully:
def batch_screenshot(urls: list[str], output_dir: str = "screenshots") -> dict:
"""
Screenshot a list of URLs, saving results to disk.
Returns a dict of {url: filepath} for successes, {url: error} for failures.
"""
Path(output_dir).mkdir(exist_ok=True)
results = {}
for i, url in enumerate(urls):
# Sanitize URL for filename
safe_name = url.replace("https://", "").replace("http://", "")
safe_name = "".join(c if c.isalnum() or c in ".-_" else "_" for c in safe_name)
filepath = f"{output_dir}/{safe_name[:80]}.webp"
try:
image_bytes = screenshot_url(url)
Path(filepath).write_bytes(image_bytes)
results[url] = filepath
print(f"[{i+1}/{len(urls)}] OK: {url}")
except requests.HTTPError as e:
if e.response.status_code == 429:
# Rate limited — wait for reset and retry once
print(f"Rate limit hit at {url}. Waiting 60s...")
time.sleep(60)
try:
image_bytes = screenshot_url(url)
Path(filepath).write_bytes(image_bytes)
results[url] = filepath
except Exception as retry_err:
results[url] = f"ERROR (retry): {retry_err}"
else:
results[url] = f"ERROR {e.response.status_code}: {e}"
except Exception as e:
results[url] = f"ERROR: {e}"
# Polite delay between requests
time.sleep(0.5)
return results
Extracting Open Graph Metadata Alongside Screenshots
Screenshots show you what a page looks like. For a complete picture, combine them with metadata extraction:
from html.parser import HTMLParser
class OGParser(HTMLParser):
def __init__(self):
super().__init__()
self.og = {}
def handle_starttag(self, tag, attrs):
if tag == "meta":
attr_dict = dict(attrs)
prop = attr_dict.get("property", attr_dict.get("name", ""))
if prop.startswith("og:"):
self.og[prop] = attr_dict.get("content", "")
def get_og_data(url: str) -> dict:
"""Fetch a page and extract Open Graph metadata."""
headers = {"User-Agent": "Mozilla/5.0 (compatible; OGAudit/1.0)"}
response = requests.get(url, headers=headers, timeout=10)
parser = OGParser()
parser.feed(response.text)
return parser.og
def audit_url(url: str) -> dict:
"""Full audit: screenshot + OG metadata."""
og = get_og_data(url)
issues = []
if not og.get("og:image"):
issues.append("missing og:image")
if not og.get("og:title"):
issues.append("missing og:title")
if not og.get("og:description"):
issues.append("missing og:description")
# Only screenshot if we have an og:image to compare against
screenshot_path = None
if not issues or "missing og:image" not in issues:
try:
img_bytes = screenshot_url(url)
screenshot_path = f"audit_{url.split('/')[-1] or 'index'}.webp"
Path(screenshot_path).write_bytes(img_bytes)
except Exception as e:
issues.append(f"screenshot failed: {e}")
return {
"url": url,
"og": og,
"issues": issues,
"screenshot": screenshot_path,
"status": "ok" if not issues else "needs_fix",
}
Practical Example: Blog Post Audit
Here's how you'd use this to audit a collection of blog posts before publishing:
posts = [
"https://example.com/blog/post-1",
"https://example.com/blog/post-2",
"https://example.com/blog/post-3",
]
results = []
for url in posts:
result = audit_url(url)
results.append(result)
status_icon = "✓" if result["status"] == "ok" else "✗"
print(f"{status_icon} {url}")
for issue in result["issues"]:
print(f" - {issue}")
# Summary
ok_count = sum(1 for r in results if r["status"] == "ok")
print(f"\n{ok_count}/{len(results)} pages passed audit")
Rate Limits and Tiers
The free tier gives you 50 requests/day — enough for spot-checking. For bulk auditing:
- Starter ($4/30 days): 200 requests/day — good for small sites
- Pro ($9/30 days): 1,000 requests/day — handles most content teams
- Business ($29/30 days): 5,000 requests/day — large-scale monitoring
All tiers are one-time payments, no subscription. Get a key at hermesforge.dev/api.
What You Can't Do With Screenshots Alone
A screenshot shows you what the page renders. It doesn't show you what a social platform will actually generate for its card — platforms cache og:image URLs aggressively and may show a different image than the one on the page today.
For social card verification, the authoritative source is each platform's debug tool (Facebook Sharing Debugger, LinkedIn Post Inspector, Twitter Card Validator). Screenshots are a useful proxy for quick audits, but not a replacement for platform-side validation before a major launch.
That said, for catching missing og:image, broken CSS on share-sized viewports, and content that doesn't fit the 1200x630 frame — automated screenshots catch real problems that HTML inspection misses.
The full API documentation is at hermesforge.dev/api. Free tier, no credit card required.