
Automatic Social Share Images: Build an OG Image Generator for Every Page
Generate a dynamic og image for every page automatically. Compare static, build-time, and on-demand OG image generators with @vercel/og, astro-og-canvas, Puppeteer, and sharp.
Every page on your site needs a unique og:image. When someone shares your link on X, Slack, WhatsApp, LinkedIn, or Discord, the platform pulls that image into the link preview. Posts with a preview image get far more clicks than bare links. Publishers commonly report a meaningful lift in click-through rate from a good preview card, and a missing or generic one is an instant credibility hit.
The problem: nobody designs 4,000 preview cards by hand. This guide shows how automatic social share images work, which generation strategy fits your stack, and how to build an OG image generator that never breaks.
Why Every Page Needs Its Own OG Image
The Open Graph protocol is a set of <meta> tags. Crawlers from social platforms read these tags when a link is shared. The three that matter most:
<meta property="og:title" content="Your Page Title" />
<meta property="og:description" content="One line of context." />
<meta property="og:image" content="https://example.com/og/my-post.png" />
When the og:image is missing, platforms fall back to whatever they can find. Sometimes that is a random logo. Sometimes it is nothing. A link with no preview image looks broken in the feed, and feeds bury links that look broken.
A per-page image does three jobs at once:
- It shows the page title in large type, so the link is readable at a glance.
- It carries your brand colors and logo, so the share is recognizable.
- It gives the platform a fixed aspect ratio, so the card renders consistently.
This is why a dynamic og image pipeline is now standard on documentation sites, blogs, and marketing pages. The image is not decoration. It is the ad for the link.
Three Strategies: Static, Build-Time, On-Demand
There are three ways to produce per-page share images. Each trades flexibility against cost.
| Strategy | How it works | Cost per page | Freshness | Best for |
|---|---|---|---|---|
| Static per page | Designer exports each PNG | High (human time) | Manual | Landing pages, few pages |
| Generated at build | Script or component renders images during deploy | Near zero | Every deploy | Blogs, docs, marketing sites |
| Generated on demand | Function renders the image when a crawler requests it | Small compute per request | Always current | Apps, user content, huge sites |
Static per page gives the best design control. It does not scale past a few dozen pages.
Build-time generation renders an image for each page when you deploy. The images ship as ordinary static files. This is the sweet spot for most content sites: zero runtime cost, perfect cache behavior, and images regenerate whenever content changes.
On-demand generation runs a rendering function when a crawler asks for the image. It suits sites where pages appear without a deploy, such as user profiles or CMS-driven apps. You pay compute per unique render, but a cache layer means each image usually renders once.
The rest of this guide covers build-time and on-demand in depth, because those are the two you automate.
The Sizing Standard: 1200×630 With Safe Margins
The Open Graph spec recommends 1.91:1. In practice, 1200×630 pixels is the standard. Every major platform crops or letterboxes gracefully at that size.
But platforms do not all show the full frame:
- X (Twitter) prefers 16:9 for large summary cards and crops the sides of 1.91:1 images.
- WhatsApp and Slack crop aggressively to a wide strip in some placements.
- LinkedIn shows the full 1.91:1 card in feeds.
The fix is a safe margin. Keep all text and your logo inside a centered zone of roughly 1024×512 pixels. Let the background color, gradient, or pattern extend to the full 1200×630. Then a side crop removes only background, never words.
Two more rules:
- Use large type. Preview cards render small on phones. A title at 64–80 px stays readable; 32 px does not.
- Keep the file small. Export as PNG for crisp text, then compress. A share image over 300 KB slows the preview on slow connections. Some platforms also time out on slow images and render the card without a picture. For photographic backgrounds, a compressed JPEG or WebP under 200 KB is often the better choice.
Build-Time Generation With @vercel/og (Satori)
@vercel/og wraps Satori, a library that renders JSX to SVG and then to PNG. You describe the card as a component. The library handles layout with a Flexbox subset.
Here is a minimal card component:
// og/Card.tsx
import satori from "satori";
export async function renderCard(title: string, description: string) {
return satori(
{
type: "div",
props: {
style: {
height: "100%",
width: "100%",
display: "flex",
flexDirection: "column",
justifyContent: "space-between",
padding: "64px",
background: "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
color: "white",
fontFamily: "Inter",
},
children: [
{
type: "div",
props: {
style: { display: "flex", fontSize: 64, fontWeight: 700, lineHeight: 1.1 },
children: title,
},
},
{
type: "div",
props: {
style: { display: "flex", fontSize: 28, color: "#94a3b8" },
children: description,
},
},
],
},
},
{
width: 1200,
height: 630,
fonts: [{ name: "Inter", data: fontBuffer, weight: 700, style: "normal" }],
}
);
}
In a Next.js App Router project, an opengraph-image.tsx file in any route segment turns this into an automatic social share image for every page under it. The framework calls your component at request time, caches the render, and emits the correct <meta> tags. You write one component; every route gets a unique card.
Satori renders to SVG first, then @vercel/og rasterizes with a built-in WebP/PNG resvg step. The result is sharp text at any density.
Build-Time Generation With astro-og-canvas
For Astro sites, astro-og-canvas generates OG images at build time using Canvas. You define one route file and it renders a card for every content entry.
This site uses it. Each guide on ImageGuide gets its own 1200×630 card at build, with the guide title and category laid out over a branded template. Nothing runs at request time, and the cards ship as static files.
The setup is a single endpoint file:
// src/pages/open-graph/[...route].ts
import { OGImageRoute } from "astro-og-canvas";
import { getCollection } from "astro:content";
const guides = await getCollection("guides");
export const { getStaticPaths, GET } = OGImageRoute({
param: "route",
pages: Object.fromEntries(guides.map((g) => [g.slug, g.data])),
getImageOptions: (_, { title, category }) => ({
template: "src/templates/og",
title,
description: category,
bgGradient: [
[30, 30, 46],
[22, 33, 62],
],
fontTitle: {
size: 72,
color: [255, 255, 255],
weight: "Bold",
},
}),
});
Then reference the generated URL from your layout:
<meta
property="og:image"
content={new AstroURL(`/open-graph/${slug}.png`)}
/>
The build renders one PNG per guide. A thousand guides cost a thousand fast Canvas draws at build time — typically well under a minute total.
On-Demand Generation With a Puppeteer Route
Satori supports a Flexbox subset, not full CSS. When you need pixel-perfect HTML — gradients, masks, custom effects — render a real browser instead. Puppeteer loads an HTML page and screenshots it.
On Vercel Functions, the route looks like this:
// api/og.ts
import puppeteer from "@sparticuz/chromium-puppeteer";
import chromium from "@sparticuz/chromium";
export const config = { runtime: "nodejs" };
export default async function handler(req, res) {
const { title } = req.query;
const browser = await puppeteer.launch({
args: chromium.args,
executablePath: await chromium.executablePath(),
});
const page = await browser.newPage();
await page.setViewport({ width: 1200, height: 630 });
await page.setContent(cardHtml(title), { waitUntil: "networkidle0" });
const buffer = await page.screenshot({ type: "png" });
await browser.close();
res.setHeader("Content-Type", "image/png");
res.setHeader("Cache-Control", "public, max-age=31536000, immutable");
res.send(buffer);
}
The tradeoffs:
- Pros: any CSS works. Your card template is just a web page, so designers can build it with normal tools.
- Cons: cold starts of one to three seconds, larger function memory (Chromium needs about 1 GB), and more failure modes than Satori.
A common pattern pairs the two: Puppeteer for the template design phase, Satori for production. Or Puppeteer on demand with a long-lived cache so the browser cost lands only on the first request per slug.
Static-Site Batch Rendering With sharp
If your site has no runtime at all and you want zero dependencies on Satori, composite SVG templates with sharp in a build script. You author the card as an SVG with placeholders, inject the title, and let sharp rasterize.
// scripts/generate-og.mjs
import sharp from "sharp";
import { readdirSync, readFileSync, writeFileSync } from "node:fs";
const posts = JSON.parse(readFileSync("posts.json", "utf8"));
const template = readFileSync("templates/og-card.svg", "utf8");
for (const post of posts) {
const svg = template
.replace("{{TITLE}}", escapeXml(post.title))
.replace("{{DESCRIPTION}}", escapeXml(post.description));
const png = await sharp(Buffer.from(svg))
.resize(1200, 630)
.png({ compressionLevel: 9 })
.toBuffer();
writeFileSync(`public/og/${post.slug}.png`, png);
console.log(`rendered ${post.slug}`);
}
function escapeXml(s) {
return s.replace(/[<>&'"]/g, (c) =>
({ "<": "<", ">": ">", "&": "&", "'": "'", '"': """ })[c]
);
}
Notes on this approach:
- SVG
<text>does not wrap automatically. Insert<tspan>lines yourself, or truncate long titles. - sharp uses librsvg, so web fonts need local installation or
@font-faceembedding as base64. - The script is dead simple to debug: the SVG is a text file you can open in a browser.
This method suits Astro, Eleventy, Hugo, and plain static sites where the build already runs Node.
Designing a Reusable Template
Whatever renderer you choose, the card is one branded template with variables. A solid template has:
| Zone | Content | Notes |
|---|---|---|
| Background | Brand gradient or pattern | Bleeds to full 1200×630 |
| Center-left | Title, up to 3 lines | Inside the 1024×512 safe zone |
| Below title | Description or category | Smaller, muted color |
| Corner | Logo or wordmark | Consistent position every time |
| Optional | Author avatar, date, series tag | Adds context for blogs |
Design rules that survive every platform:
- Contrast first. White text on a dark gradient reads everywhere. Thin gray text does not.
- One idea per card. The title is the message; skip decorative charts.
- Test the crop. Screenshot your card, then crop the outer 88 px from each side. If the card still works, your safe margins are right.
Font Loading Gotchas in Satori
Satori is the pickiest part of any OG image generator about fonts:
- No system fonts. Satori never reads fonts from the OS. You must load every font file as a buffer.
- Every weight is a separate file. Regular, bold, and italic each need their own registration entry.
- Load once, reuse. Fetch font files at module scope, not inside the render function. On serverless, cache the buffer in module scope or the cold start pays for the download on every request.
- Variable fonts work, but declare the weights you use.
const fontData = await fetch(
"https://cdn.example.com/fonts/inter-bold.woff2"
).then((r) => r.arrayBuffer());
If text renders as squares or disappears, the font was not loaded — that is the cause nine times out of ten.
Emoji Support
Satori does not render emoji from text fonts. Add @vercel/og/twemoji support or load a color emoji font such as Noto Emoji. In @vercel/og, pass emoji: "twemoji" to the options and emoji render as inline images. In raw Satori, load Noto Color Emoji as an extra font entry. Puppeteer and Canvas render emoji natively, which is one reason teams pick them despite the heavier runtime.
Caching: Content-Addressed by Slug
A share image depends only on its slug and template version. That makes it perfectly cacheable:
Cache-Control: public, max-age=31536000, immutable
For build-time pipelines this is free: the file exists at a stable URL like /og/my-post.png and any CDN serves it from edge.
For on-demand pipelines, make the URL content-addressed:
/og/my-post.png— the slug is the cache key. Same slug, same image, forever.- When the template changes, you have two options: bump a version segment (
/og/v2/my-post.png) or purge the cache. Versioned URLs are simpler and safer.
If the card embeds live data (view counts, prices), shorten max-age to hours and accept the re-render cost. Otherwise immutable caching means each image renders at most once per deployment per edge node.
Also set og:image:width and og:image:height in the meta tags. Crawlers skip a download round trip when the dimensions are declared, so previews render faster.
Debugging: Validate Before You Ship
Crawlers cache aggressively. A broken card can stick for days, so validate before and after every change:
- Facebook Sharing Debugger (Meta Sharing Debugger) — scrapes the URL fresh, shows the exact card, and lets you force a re-scrape. Use this after any fix.
- X Card Validator — previews the X card. Note that X sometimes requires the page to be publicly reachable with no auth walls.
- LinkedIn Post Inspector — shows LinkedIn’s view and also refreshes their cache.
The usual failure causes, in order of frequency:
- Relative image URL.
og:imagemust be an absolutehttps://URL. - Image behind auth, robots-blocked, or on a host that blocks crawler user agents.
- Image too large or too slow, so the crawler times out.
- Meta tags injected client-side after the crawl. Crawlers read initial HTML; render tags on the server.
- Wrong content type on the image response.
Edge Cases Worth Knowing
Multi-image. Open Graph supports multiple og:image tags, and Facebook may show a gallery. Most other platforms show only the first. Put your best card first and treat the rest as a bonus. In practice, one excellent image beats three average ones.
Video thumbnails. For og:video content, platforms still want og:image as the poster frame. Generate the poster from the video at a fixed timestamp, at 1200×630, and set both tags. A video without a poster image often shows no preview at all.
Per-platform aspect preferences. X favors 16:9 and crops 1.91:1 from the sides. LinkedIn and Facebook show the full 1.91:1. WhatsApp crops to roughly 1.7:1 in some clients. The 1200×630 canvas with wide safe margins covers all of them from one asset. If X is your primary channel and you need pixel-perfect presentation, add twitter:image with a separate 1200×675 render — but most sites skip this and accept the minor crop.
Long titles. Truncate at render time with an ellipsis, not in CSS. A card with clipped text looks broken; a card with a deliberate “…” reads as designed.
Choosing: Static Site vs SSR vs Headless CMS
| Your setup | Recommended approach | Why |
|---|---|---|
| Static site (Astro, Eleventy, Hugo) | Build-time: astro-og-canvas, sharp script, or Satori in a build step | Zero runtime cost; cards are static files with immutable caching |
| Next.js / SSR app | On-demand: @vercel/og route or opengraph-image.tsx |
Framework integration handles caching and meta tags; always fresh |
| Headless CMS with editorial preview | On-demand with short cache | Preview cards must reflect unpublished drafts; cache in minutes, not years |
| Huge docs site (10k+ pages) | Build-time with parallel workers, or on-demand + CDN cache | Build-time keeps runtime at zero; on-demand avoids long builds |
| User-generated pages | On-demand, content-addressed URLs | Pages appear without deploys; slug keying keeps renders to one per page |
The decision reduces to one question: when does the page content become final? If the answer is “at deploy,” generate at build. If the answer is “whenever the user acts,” generate on demand behind a cache.
Wrapping Up
An OG image generator is a small investment with outsized returns: every shared link becomes a branded, readable card. Start with a build-time pipeline if your site has a build. Move to @vercel/og or Puppeteer only when pages must render without a deploy. Keep 1200×630, keep text inside the safe zone, cache immutably, and validate with the platform debuggers before your audience finds the bug for you.
Once your share images are automated, the same discipline pays off across your whole image pipeline — sizing, compression, and delivery all follow the pattern this guide showed.
To speed up the rest of your image work — galleries, zoom, and 360 spins on your product pages — see the Sirv Media Viewer, and for AI-assisted editing like background removal and alt text generation, check out Sirv Studio. You can create a free Sirv account and put your images on a fast CDN in minutes.