Build-time og:image generation with eleventy-img
I’ve started incorporating sample PNGs to illustrate typefaces in the Atlas of Type, and secretly hoped they’d work as OpenGraph previews just as well. Turns out tightly cropped, black-on-transparent images like the one below were not on most platforms’ og:image
bingo cards:

The sample PNG for the recently-released Betània Patmos, emphasised with a 1px border.
The results were uniformly atrocious. To render well, the samples need a solid background and some padding:

As the Atlas is a 100% static Eleventy website, these preview images need to be computed beforehand, ideally without manual involvement. A scan of the docs for the trusty eleventy-img
plugin revealed that the 6.x release last week coincidentally added access to sharp
, the underlying image processing library, so compositing the sample PNGs onto a white canvas was no big deal.
This is the .eleventy.js
config to define an og_image
shortcode that takes an image from static/img/typeface/
and generates a corresponding 1200×800
preview image in static/img/card/
:
import path from 'node:path';
import ImagePlugin from '@11ty/eleventy-img';
const TARGET_IMG_WIDTH = 1200;
const TARGET_IMG_HEIGHT = 800;
config.addShortcode('og_image', async function(src) {
const inputPath = path.join('static', src);
const img = await ImagePlugin(inputPath, {
formats: ['png'],
urlPath: '/img/card',
outputDir: 'static/img/card',
transform: async sharp => {
const metadata = await sharp.metadata();
const pad_width = (TARGET_IMG_WIDTH - metadata.width) / 2;
const pad_height = (TARGET_IMG_HEIGHT - metadata.height) / 2;
sharp
.flatten({ background: 'white' })
.extend({
top: Math.floor(pad_height),
bottom: Math.ceil(pad_height),
left: Math.floor(pad_width),
right: Math.ceil(pad_width),
background: 'white'
});
},
// disable hashing, return original file name.
filenameFormat: function (id, src) {
return path.relative('static/img', src);
}
});
return img.png[0].url;
});
Note: I’m probably losing some performance by disabling the built-in hashing, but I wanted predictable URLs for the og:image
s. If you don’t care about that, remove the custom filenameFormat()
function.
Usage in a Nunjucks template, and the resulting markup:
<!-- template -->
<meta
property="og:image"
content="{{ site.url }}{% og_image '/img/typeface/my-sample.png' %}"
>
<!-- result -->
<meta
property="og:image"
content="https://type-atlas.xyz/img/card/typeface/my-sample.png"
>
Addendum to point out a small gotcha. Because the og:image
cards are put back in the static/
folder, which itself gets duplicated via Passthrough File Copy into the output folder, the first build with new source images won’t see the freshly generated cards. Eleventy 2.x added the ability to skip the actual copying in serving mode, and opt into serving the files from their original locations. This seems like a generally good addition to your Eleventy config:
config.setServerPassthroughCopyBehavior("passthrough");
This is for local development. As for deployment, make sure at least one prior build has run so cards for new images have a chance to get generated.