When a link to your site gets pasted into Slack, iMessage, X, or LinkedIn, the platform unfurls it into a card with a title, a description, and a picture. That picture is your Open Graph image, and it does a lot of the work of deciding whether anyone clicks. Hand-designing one image per blog post, product page, or changelog entry doesn't scale past a handful of pages. The fix is to design the card once as an HTML template, interpolate the dynamic bits, and render it to a PNG on demand.
This guide walks through the whole pipeline: the meta tags, the template, and a single API call that turns markup into a pixel-exact 1200×630 image.
What an Open Graph image is
An Open Graph image is referenced by two meta tags in your page <head> — og:image for the Open Graph protocol (Facebook, LinkedIn, Slack, iMessage) and twitter:image for X's card format. Most platforms read both, so set both and point them at the same URL.
<meta property="og:image" content="https://yoursite.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" />
<meta name="twitter:image" content="https://yoursite.com/og/my-post.png" />
The canonical size is 1200×630 — a 1.91:1 ratio. That's the dimension X, LinkedIn, and Slack crop toward, and the one the summary_large_image Twitter card expects. Render anything else and you risk letterboxing or an awkward center-crop. Pin 1200×630 and you control exactly what people see.
The approach: HTML template → image
Instead of opening a design tool for every page, build one HTML/CSS card and leave placeholders for the parts that change — usually the title, maybe an author, a category, or a date. Your code fills those placeholders in, and a renderer turns the resulting markup into a PNG.
Here's a minimal card. It's just a div sized to the OG dimensions with whatever layout and branding you want inside it.
<div style="width:1200px;height:630px;display:flex;flex-direction:column;
justify-content:space-between;padding:80px;box-sizing:border-box;
font-family:'Hanken Grotesk',system-ui,sans-serif;
background:#0A0A12;color:#F7F7FB;">
<div style="font-family:'JetBrains Mono',monospace;font-size:24px;
letter-spacing:2px;color:#9B8CFF;">SCREENSHOTINK · GUIDES</div>
<h1 style="font-size:72px;line-height:1.1;font-weight:700;margin:0;">
How to Automatically Generate Open Graph Images
</h1>
<div style="font-size:28px;color:#A9A9B8;">By ScreenshotInk</div>
</div>
Because it's plain HTML and CSS, you can iterate on the design in a browser, then hand the same markup to the renderer with no translation step. Web fonts, gradients, flexbox, and CSS variables all work.
Generate it with one API call
ScreenshotInk's /v1/capture endpoint accepts raw HTML directly via the html parameter, so you never have to host the template anywhere — just POST the markup and ask for the exact dimensions you want.
curl "https://screenshotink.com/v1/capture" \
-H "Authorization: Bearer sk_live_…" \
--data-urlencode html="<div style='...'>…</div>" \
-d width=1200 -d height=630 -d full_size=false -d format=png
Two things make this exact. Setting full_size=false tells the renderer not to expand the canvas to fit the page's natural height — it clips to the viewport instead. Combined with width=1200 and height=630, that pins the output to precisely 1200×630, regardless of how your markup would otherwise flow. The response is JSON with a hosted URL:
{ "image_url": "https://…/abc123.png" }
Drop that URL straight into your og:image and twitter:image tags.
Here's the same call from Node, building the card string per page:
function buildCard({ kicker, title, author }) {
return `<div style="width:1200px;height:630px;display:flex;
flex-direction:column;justify-content:space-between;padding:80px;
box-sizing:border-box;font-family:'Hanken Grotesk',sans-serif;
background:#0A0A12;color:#F7F7FB;">
<div style="font-family:'JetBrains Mono',monospace;font-size:24px;
letter-spacing:2px;color:#9B8CFF;">${kicker}</div>
<h1 style="font-size:72px;line-height:1.1;font-weight:700;margin:0;">
${title}</h1>
<div style="font-size:28px;color:#A9A9B8;">By ${author}</div>
</div>`;
}
async function generateOgImage(page) {
const body = new URLSearchParams({
html: buildCard(page),
width: '1200',
height: '630',
full_size: 'false',
format: 'png',
});
const res = await fetch('https://screenshotink.com/v1/capture', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.SCREENSHOTINK_KEY}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
});
const { image_url } = await res.json();
return image_url;
}
const url = await generateOgImage({
kicker: 'SCREENSHOTINK · GUIDES',
title: 'How to Automatically Generate Open Graph Images',
author: 'ScreenshotInk',
});
Remember to escape or sanitize any user-supplied text before interpolating it into the markup.
Wire it into your build or request flow
There are two clean places to call this.
At build time. If your site is static or rebuilt on each deploy, generate one image per page during the build, write the PNG URL into the page's metadata, and you're done. The cards are ready before the first visitor arrives, and there's no runtime dependency.
On the fly, per request. For dynamic pages — user profiles, generated reports, anything you can't enumerate ahead of time — call the API the first time a card is requested and cache the result. Identical requests to ScreenshotInk are cached for 24 hours, so regenerating the same card within that window costs you nothing; the endpoint returns the existing image. Either way, persist the resulting image_url (in a database column, a key-value store, or a CDN path) so you're not hitting the API on every page load.
We dogfood this exactly — the OG cards on ScreenshotInk's own pages are rendered through /v1/capture from the same kind of HTML template shown above. Infrastructure is EU-hosted, and the free tier gives you 100 captures a month, which is plenty to template every page on a small-to-mid site.
Try it: Open Graph image generator. If you want to render arbitrary markup to a raster more generally, the HTML to image tool uses the same engine.
Capture it with the API
Everything here runs on the ScreenshotInk API — 100 free captures a month, no card.