实操
How to generate OG images at build time
Produce share cards in a build step: one template, a loop over your content, and a content hash so nothing re-renders unless content changes.
更新于 2026年4月18日 · 约 3 分钟
Doing share cards by hand works until the hundredth article. After that the images drift: some pages get a card, some keep last year’s template, and nobody knows which. Generating them in a build step fixes that with one template, a loop over your content, and a rule for when to re-render.
The three ways to make the image
| Approach | Layout engine | Design ceiling | Speed and weight | Best for |
|---|---|---|---|---|
| Satori plus a rasteriser | A subset of CSS, flexbox only | Constrained but consistent | Fast, small, edge-friendly | Hundreds of near-identical cards |
| Headless browser | The full CSS engine | Full | Slow, heavy, needs a browser | Templates that use grid, effects or unusual fonts |
| Hand-designed export | None | Unlimited | Manual | A single flagship card |
@vercel/og packages the first row — Satori for layout, a WebAssembly rasteriser for the PNG — behind a function that returns an image. Satori is the reason the output is predictable: it understands a subset of CSS, lays out with flexbox and not grid, needs an explicit canvas size, and cannot reach system fonts. Those constraints are also its costs.
What a build-time generator looks like
The shape is the same in any framework:
- Read every content entry: title, description, author, slug.
- Render a template to an image.
- Write it to a stable path, ideally derived from the entry.
- Emit the
og:imagetag pointing at it.
// 只在内容或模板变化时重渲染;输出用内容哈希命名
import { createHash } from 'node:crypto';
import { satoriToPng } from './renderer'; // 封装 satori + resvg 的小工具
const TEMPLATE_VERSION = 3; // 改模板时手动加一,让全部卡片失效
const hash = (input: string) =>
createHash('sha256').update(input).digest('hex').slice(0, 8);
export async function renderCard(entry: { title: string; summary: string }) {
const key = hash(`${TEMPLATE_VERSION}:${entry.title}:${entry.summary}`);
const out = `public/og/${key}.png`;
if (await exists(out)) return `/og/${key}.png`; // 命中缓存,跳过渲染
await writeFile(out, await satoriToPng(entry));
return `/og/${key}.png`;
}
The hash is the whole trick. It folds the template version into the file name, so editing the template produces new URLs for every card, and editing one article produces a new URL for exactly one.
Caching and incremental generation
- Name by content, not by slug. A slug-named file changes only when you overwrite it, which is exactly the case every cache is built to ignore.
- Keep a manifest. A small JSON file mapping each entry to its last hash lets a build skip everything unchanged, which matters once you have thousands of pages.
- Cache the render in CI. If your runner has a cache, store the output directory under a key that includes the template version.
- Bump the template version deliberately. Font, spacing and colour changes need a manual increment, because the build cannot see the template as an input unless you tell it to.
Re-rendering when an article changes
Because the file name comes from the title and summary, editing either one produces a new URL on the next build. The page now points at a URL no cache has seen, so platforms fetch the new image instead of serving the old one. That is the same caching problem that makes a swapped image look unchanged; the meta preview debugger shows which image a platform currently holds.
Common mistakes
Naming files by slug. Overwriting og/<slug>.png keeps the URL constant, and a constant URL means every cache keeps serving the old bytes. Fold a hash of the content into the name.
Forgetting the font. A Satori-style renderer cannot use system fonts. If you do not load one, the render fails or falls back to something you did not design.
Rendering a design the engine cannot express. Grid layouts, blend modes and complex shadows are outside the subset. Either simplify the template or move to a headless browser.
Rendering per request when build time would do. If titles only change on deploy, generating on every request wastes compute and makes the card harder to cache.
Emitting the tag but not the file. A path that 404s gives a text-only card. Assert during the build that every referenced image exists.
No template version. Change the design, and unchanged pages keep the old cards because their hash never moved.
Where this tool fits
The OG image generator is the manual counterpart: when you need one card and not a pipeline, it renders the 1200×630 canvas in the browser and hands you the tags. For the rules the template should follow, see OG image sizes for every platform. Once deployed, verify the result with the meta preview debugger.
Frequently asked questions
▸ Should I generate OG images at build time or on demand?
Build time when your content changes on deploy and you want stable, cacheable URLs. On demand when titles vary per request or you cannot enumerate the pages. Build time is simpler to cache and to debug.
▸ What renders the image in a build step?
Either an HTML-and-CSS renderer such as Satori, with a rasteriser like resvg or sharp behind it — which is what @vercel/og packages — or a headless browser such as Playwright when you need the full CSS engine.
▸ How do I stop regenerating every image on every build?
Name each output by a hash of its inputs plus a template version, keep a manifest of what already exists, and skip the render when the hash matches. A template change bumps the version and refreshes the set.
▸ What are the limits of a Satori-style renderer?
It supports a subset of CSS and lays out with flexbox only, needs an explicit canvas size, and cannot use system fonts — you must supply the font files. A headless browser removes those limits at the cost of speed and weight.
▸ How do platforms pick up a regenerated image?
Give the file a new name when its content changes. A content hash in the path means every fresh card is a fresh URL, which sidesteps the platform and CDN caches that still hold the old one.