Guides · 2026-10-17
Dynamic Open Graph images from HTML templates (that cost almost nothing)
Open Graph images — the preview cards that appear when a link is shared on X, LinkedIn, Slack, Discord or iMessage — measurably increase click-through. Generating one per blog post, product or user profile by hand doesn't scale. The cleanest approach: design the card as an HTML template, then render it to a 1200×630 PNG on demand. Here's how, plus the caching trick that keeps it nearly free.
The spec in 30 seconds
- Size: 1200 × 630 px (1.91:1). Everyone accepts it; keep important content away from the outer ~60 px since some apps crop.
- Format: PNG for crisp text, or JPEG/WebP if you include photos. Keep it under ~1 MB; some platforms reject larger images.
- Tags:
<meta property="og:image" content="https://example.com/og/my-post.png"> <meta property="og:image:width" content="1200"> <meta property="og:image:height" content="630"> <meta name="twitter:card" content="summary_large_image">
Crawlers fetch the image URL themselves, so it must be a public, absolute URL that returns an image quickly.
Step 1: build the card as HTML
You already know how to make things look good in HTML and CSS — use it. A minimal template:
<html><head>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@500;800&display=swap" rel="stylesheet">
<style>
body { margin:0; width:1200px; height:630px; display:flex; flex-direction:column;
justify-content:center; padding:0 80px; box-sizing:border-box;
font-family:Inter; background:#0b0d17; color:#fff; }
h1 { font-size:64px; line-height:1.1; margin:0 0 24px; }
p { font-size:28px; color:#9aa0bd; margin:0; }
</style></head>
<body><h1>{{ title }}</h1><p>{{ author }} · {{ date }}</p></body></html>
Serve it from a route in your app, e.g. /og-template?slug=my-post, filling in the title from your database. Open it in a browser at 1200×630 to iterate on the design.
Step 2: render it to PNG
Any headless browser can do it. With Playwright:
page = browser.new_page(viewport={"width": 1200, "height": 630})
page.goto(f"https://example.com/og-template?slug={slug}", wait_until="networkidle")
png = page.screenshot(type="png")
The catch: social crawlers hit your image URL at unpredictable times, often in bursts when a post goes viral. Running a browser synchronously on each hit is slow and expensive — so cache aggressively, and ideally serve from a CDN.
Step 3: or point og:image straight at a rendering API
With Shotlane you can skip the rendering server entirely and put a signed URL directly in your og:image tag. The signature (HMAC with your secret key) means nobody can tamper with the parameters or use your account to render other pages; your secret never appears in the HTML.
# Python — generate the og:image URL when rendering your page
import hmac, hashlib
from urllib.parse import urlencode
def og_image_url(slug):
params = {"url": f"https://example.com/og-template?slug={slug}",
"width": 1200, "height": 630, "wait_until": "networkidle",
"cache_ttl": 2592000} # 30 days
qs = urlencode(sorted((k, str(v)) for k, v in params.items()))
sig = hmac.new(SHOTLANE_SECRET.encode(), qs.encode(), hashlib.sha256).hexdigest()
return f"https://shotlane.dev/v1/screenshot?{qs}&key={SHOTLANE_PUBLIC_KEY}&signature={sig}"
The crucial part is cache_ttl: the first crawler hit renders the image (1 credit), and every hit after that for the next 30 days is served from cache for free. A post shared 10,000 times still costs a single credit — so a $9 pack covers about 2,000 posts' worth of preview images, with no expiry. When you change a post's title, add a version parameter (e.g. &v=2 in the template URL) to get a fresh render.
Step 4: test it
- Paste your page URL into the sharing debuggers: LinkedIn Post Inspector, Facebook Sharing Debugger, or simply send it to yourself in Slack.
- Platforms cache previews aggressively too — use their "re-scrape" button after changes.
- Check the image URL directly in a browser: it should return a PNG in well under the ~5 second limit some crawlers apply. Cached responses return in milliseconds.
Want to see how your template renders before writing any code? Paste its URL into the free screenshot tool, or read about signed URLs in the docs.
Skip the browser babysitting
Shotlane runs hardened headless Chromium for you: screenshots and PDFs from one HTTP request, with cookie-banner hiding, ad blocking, signed embed URLs and free cache hits. Prepaid credits that never expire.
100 free renders · no credit card · key shown instantly