Dynamic OG Images
Auto-generate a unique social preview card for every page of your blog, SaaS, or e-commerce site.
The problem
When someone shares a URL on Twitter, LinkedIn, or Slack, the platform fetches the og:image meta tag and renders a preview card. A single static image doesn't scale — you need a unique, branded card for every blog post, product, or user profile.
The classic solutions are either slow (headless browser per request) or fragile (canvas hacks). StarkRender gives you a REST API: design your card once in HTML, save it as a template, and call the API with each page's data. You get back a permanent URL to use as og:image.
Step 1 — Design the card in HTML
The standard OG image size is 1200 × 630 px. Design your card as a self-contained HTML snippet. Use {{variable}} placeholders for the dynamic parts.
<div style="
width:1200px; height:630px;
background:#0f172a;
display:flex; flex-direction:column;
justify-content:flex-end;
padding:60px;
font-family:Inter, sans-serif;
box-sizing:border-box;
">
<p style="color:#94a3b8;font-size:20px;margin:0 0 16px">
{{category}}
</p>
<h1 style="color:#fff;font-size:52px;font-weight:800;line-height:1.15;margin:0 0 28px">
{{title}}
</h1>
<div style="display:flex;align-items:center;gap:16px">
<img src="{{author_avatar}}" width="44" height="44"
style="border-radius:50%;object-fit:cover">
<div>
<p style="color:#fff;font-size:18px;font-weight:600;margin:0">{{author_name}}</p>
<p style="color:#64748b;font-size:16px;margin:4px 0 0">{{date}}</p>
</div>
</div>
<div style="position:absolute;top:48px;right:60px;color:#C9952A;font-size:22px;font-weight:700">
YourBrand
</div>
</div>
Step 2 — Save the template (once)
Call POST /v1/template to store the HTML. You get back a template_id — save it in your environment config. You only need to do this once.
curl -X POST https://api.starkrender.com/v1/template \ -H "x-api-key: YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"name": "Blog OG card", "html": "<!-- your template HTML -->"}'
{
"status": "success",
"template_id": "86c4dab7-6570-442a-9a46-74bb2a472aed"
}
Step 3 — Render per page
On each new post or page, call POST /v1/render with the template ID and the page's data. Pass device_scale: 2 for retina-quality output (Twitter and LinkedIn both support it).
Node.js
async function generateOgImage(post) {
const res = await fetch('https://api.starkrender.com/v1/render', {
method: 'POST',
headers: {
'x-api-key': process.env.STARKRENDER_API_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
template_id: '86c4dab7-6570-442a-9a46-74bb2a472aed',
variables: {
title: post.title,
category: post.category,
author_name: post.author.name,
author_avatar: post.author.avatarUrl,
date: post.publishedAt, // e.g. "May 8, 2026"
},
width: 1200,
height: 630,
device_scale: 2, // retina — 2400×1260px output
}),
});
const { url } = await res.json();
return url; // persist this — point og:image here
}
Python
import os, requests
def generate_og_image(post: dict) -> str:
resp = requests.post(
"https://api.starkrender.com/v1/render",
headers={
"x-api-key": os.environ["STARKRENDER_API_KEY"],
"Content-Type": "application/json",
},
json={
"template_id": "86c4dab7-6570-442a-9a46-74bb2a472aed",
"variables": {
"title": post["title"],
"category": post["category"],
"author_name": post["author"]["name"],
"author_avatar": post["author"]["avatar_url"],
"date": post["published_at"],
},
"width": 1200,
"height": 630,
"device_scale": 2,
},
)
resp.raise_for_status()
return resp.json()["url"] # persist this — point og:image here
Step 4 — Wire up the meta tag
Store the returned URL alongside your post in the database. Then render it into your page's <head>.
<meta property="og:image" content="https://api.starkrender.com/v1/image/uuid"> <meta property="og:image:width" content="2400"> <meta property="og:image:height" content="1260"> <meta name="twitter:card" content="summary_large_image"> <meta name="twitter:image" content="https://api.starkrender.com/v1/image/uuid">
Tips
Generate on publish, not on request
Call the API when a post is created or updated — not on every page view. Store the returned URL in your database alongside the post. This keeps latency at zero for readers and avoids unnecessary renders.
Use device_scale: 2 for retina
Twitter, LinkedIn, and most modern scrapers support high-DPI images. Passing device_scale: 2 produces a 2400 × 1260 px image from a 1200 × 630 layout — sharper text and edges with no extra work on your end.
Long titles — clamp with CSS
Add overflow protection to your template so long titles don't break the layout:
h1 {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
Batch-generate for existing content
If you need to backfill OG images for existing posts, use POST /v1/render/batch to render up to 25 at once in a single request — all processed in parallel.
const res = await fetch('https://api.starkrender.com/v1/render/batch', { method: 'POST', headers: { 'x-api-key': API_KEY, 'Content-Type': 'application/json' }, body: JSON.stringify({ requests: posts.map(post => ({ template_id: TEMPLATE_ID, variables: { title: post.title, category: post.category, ... }, width: 1200, height: 630, device_scale: 2, })), }), }); const { results } = await res.json(); // results[i].url → og:image for posts[i]
StarkRender