Generating preview images automatically

Three viable approaches. One static image for the whole site, which is fine for small sites and takes an afternoon. Build-time generation, one image per page, produced when the site builds — the right default for static sites. On-demand generation, rendered by a server the first time each image is requested, for sites whose pages are created dynamically.

Pick by page count and how pages come into existence. All three beat the common alternative, which is pages with no preview image because making one was a manual task nobody did.

One static image

A single file, referenced by every page.

Every share looks the same, so the card says nothing about the specific page. Against that: it takes one afternoon, it never breaks, there’s no build step, and it’s strictly better than no image.

Reasonable for a site with a handful of pages, or as a fallback beneath either approach below. Not reasonable for a blog with a hundred posts, where identical cards make every link look like every other link.

Build-time generation

At build time, for each page, render an image from a template and the page’s title, then write it to your output directory.

This is the right default for anything statically generated. The reasons:

The image is a static file. No server, no runtime cost, cached like any other asset, and it can’t fail at share time.

Failures happen at build. If generation breaks, your build breaks, and you find out before publishing rather than when a card renders empty.

Content-hashed filenames come free. Your build already does this for other assets, and it solves image cache invalidation permanently — a changed image is automatically a changed URL.

The mechanics, in whatever tooling you have: define a template with a text slot, iterate your pages, render each to PNG, write it to a predictable path, and emit og:image pointing there.

The cost is build time. Rendering an image per page adds up over hundreds of pages, so cache by content hash and only regenerate what changed. Most build systems make this straightforward and it’s worth setting up early.

On-demand generation

An endpoint that renders an image per request, typically taking the title as a parameter:

https://example.com/og?title=Generating+preview+images

The URL goes straight into og:image. First request renders and caches; later requests serve the cached copy.

Right when pages are created dynamically — user-generated content, a large catalogue, anything where a build-time pass isn’t possible.

Three things to get right:

Cache aggressively. Preview crawlers can request the same image repeatedly. Without a cache in front, you’re rendering an image per crawl.

Validate the parameters. An endpoint that renders arbitrary text into an image, at an arbitrary URL, is an open invitation to put someone else’s words on your domain. Sign the parameters, or restrict input to known page identifiers rather than free text.

Have a fallback. If rendering fails or times out, serve a static default rather than an error. A crawler that gets a 500 caches a card with no image.

What renders the image

Two families, and the trade-off is fidelity against weight.

HTML and CSS, rendered by a headless browser. You describe the card in the technology you already know, with real text layout and web fonts. Costs a browser in your build or runtime — heavy, slow to start, and the most common source of “works locally, fails in CI”.

A drawing library. Direct canvas or image-library calls. Much lighter and faster, no browser, and you implement text layout yourself — which means word wrapping and font metrics become your problem.

For a template with one text slot, the drawing-library route is usually less trouble than it sounds and avoids a large dependency. For anything with real layout, the browser route pays for itself.

Either way, embed the fonts you use rather than relying on system fonts. A generator that renders correctly on your machine and in a fallback font on the build server is a classic and confusing failure.

Getting the text to fit

The one problem every generator hits: titles vary in length and the slot doesn’t.

Options, roughly in order of preference:

Shrink to fit. Reduce the font size until the text fits the box, with a floor below which you stop. Simple, and it degrades gracefully.

Wrap to a fixed number of lines, then truncate with an ellipsis. Predictable, and it can cut a title mid-thought.

Author a separate short title for the image. Best results, requires a human per page, which is the thing you were automating away.

A combination works well: shrink to fit within two or three lines, truncate beyond that, and allow an optional per-page override for the handful of pages where it matters.

Verify the output

Generated images fail quietly, so build in a check:

  • Assert the file exists and is non-trivial in size for every page. Catches silent generation failures.
  • Look at a sample after any template change. Automated generation means a template bug affects every page at once.
  • Confirm the declared dimensions match what you actually produce.
  • Check the fonts rendered rather than falling back — most visible in a diff of two sample images.

An automated pipeline turns one mistake into a site-wide mistake, which is a good trade as long as something is looking at the output.