Vol. XII · No. 05 · May 2026
Jake Cuth.

33 OG images in 80 lines of Python.


Per-page OpenGraph cards used to mean a Figma template and an afternoon. The afternoon part is the killer — not because the work is hard, but because the work is dull, and so it never gets done, and so every page on your site shares one anonymous 1200×630 default image until the day a tweet finally embarrasses you into fixing it.

The site you're reading already had a generic og-image.png. Today it has 33 lab-specific cards plus a homepage default. The script that produced them is 80 lines of Pillow, runs in 4 seconds, reads each lab's existing <meta property="og:title"> and FIG number, and writes a graphite-themed PNG with the title typeset in serif and a small amber section marker in the corner. No new metadata to maintain. No design tool. No build step.

The trick is committing to one template aggressively. Every card has the same structure:

┌─────────────────────────────────────┐
│ JAKECUTH.COM           § FIG. 04    │
│                                     │
│   A/B Test Simulator                │
│   run the math, not the hype        │
│                                     │
│ ──────────────────────────────────  │
│ DATA SYSTEMS THAT SHIP   ● ML · NYC │
└─────────────────────────────────────┘

Title in Liberation Serif (DejaVu and Newsreader work too), italic kicker after the em-dash, FIG number in DM Mono small caps, one bottom rule. The font sizer auto-shrinks the headline through a step ladder — 96, 88, 80, 72, 66, 60, 54 — until the wrap fits in three lines or fewer. Words like “Semiconductor Cartography” get the small treatment; “DBSCAN” gets the large one. No one card is special.

Two anti-patterns to skip on the way:

Pulling fresh metadata from a sidecar JSON. You don’t need it. Your HTML already has the title and description in the head. A regex over og:title and og:description is all the inventory you need.

Generating SVG and converting to PNG. Pointless indirection. Pillow draws to PNG natively, the typography is already locked by the chosen TTF, and saving is one method call. The SVG path adds rsvg-convert or a headless browser to your build for no benefit.

The script lives at notebooks/generate_og_images.py and the wiring step (which rewrites every lab's og:image line in place) is next door at notebooks/wire_og_images.py. Run them in order and your social previews stop being lies.

There’s a small caveat: the OG image URL has to be crawler-reachable. Your social card preview is only as good as your weakest DNS record.

python seo infra