For AI agents and crawlers: a structured index of this site is available at https://danny.is/llms.txt.

Nicer Cover Images

I gave this site’s OG Images a makeover a few weeks back. These are images that show up when you share a link to a post on Slack, Discord, Bluesky and the like. The old ones were hastily thrown together ages ago and looked pretty rubbish. The new ones include a title, my avatar, the post’s URL and my name. Here’s an example from a recent note

Example of an OG Image

They’re generated at build time by og-image-generator.ts which:

  1. Loads the TTF fonts.
  2. Turns the background SVG and avatar into data URIs.
  3. Calls og-templates.ts which returns an element tree including all the text, structure and styling.
  4. Runs Satori to convert the element tree into an SVG.
  5. Uses resvg to convert the SVG into a PNG.

I went with Tree → SVG → PNG because of the fonts. Satori converts all the text into vector paths, so by the time the image is rasterised there are no fonts to resolve. This avoids a whole set of font-rendering issues I ran into before when I was hand-writing SVGs and rasterising them directly with sharp.

Making the title fit

The fiddly bit was the title because it needs to be as big as possible without overflowing, and Satori has no “shrink to fit”. It’s not perfect, but I ended up with some code which roughly estimates how wide each letter is, tries wrapping the title at the biggest size, and then stepping down until it fits its box:

function glyphEm(ch: string): number {
if (ch === ' ') return 0.3;
if (`.,:;'!|iIlj`.includes(ch)) return 0.3; // skinny
if ('MWmw@%'.includes(ch)) return 0.92; // wide
return 0.64; // everything else
}
// Try the biggest size, shrink by 2px until the lines fit.
for (let size = 96; size > 44; size -= 2) {
if (titleFitsAt(size)) return size;
}

It’s deliberately rough and Satori still does the actual wrapping. The URL is easier: it’s set in a monospace font, so every character is the same width and I can work out the right size directly.

Here’s an example of a short title:

short-og-example.png

And here’s a stupidly long title:

long-og-example.png

And here’s the default, which is statically served but can be regenerated based on the same templates with a manual script.

og-default-example.png

The whole thing is now cached too, so runs much faster than it used to. This is a tiny little tweak, but I’m pleased with how they look – especially when shared on Bluesky with my new atproto records

bluesky-og-image-demo.png

copy / view as markdown