Automations and webhooks for the AT Protocol airglow.run

Brand fonts under /fonts/ return 404 in production #9

Closed tyb.there-hugo.dev
Closed opened by tyb.there-hugo.dev

Problem #

lib/runtime/web.ts:58-71 only serves /static/*, /favicon.svg and /og-image.png. _renderer.tsx:122-142 and global.css.ts reference /fonts/*.woff2 from public/fonts/.

curl https://airglow.run/fonts/OverusedGrotesk-VF.woff2 returns 404 (checked 2026-10-17).

Seen in the session #

In https://www.youtube.com/watch?v=5MnT-ZgbBIc:

  • 1:16 to 1:23: font 404s in the open devtools console.

Fix direction #

Serve /fonts/* from the production runtime, or move the fonts under /static/.

Details: docs/brainstorms/2026-10-17-graze-conference-automation-feedback.md (B5).

Queued: the loop will work on this version of the issue.

loop:queued cid=bafyreihbtb652x6g6fy6qehjwpp7sifxfpbwlwc6ep3rvip2nau5pprbx4

Working on this, attempt 1.

loop:claim run=contribute-20261023-221235-3my32eycidr22 branch=loop/3my32eycidr22 attempt=1

fix(runtime): serve brand fonts under /fonts/ in production

The production runtime only mounted /static/*, /favicon.svg and /og-image.png from dist/, so the brand fonts that _renderer.tsx preloads and global.css.ts loads from /fonts/*.woff2 returned 404 in production while Vite served them fine in dev. lib/runtime/web.ts now mounts /fonts/* from the build output too. The asset mounts moved into an exported mountBuiltAssets(app, root) so a test can run them against public/ without a real build.

Verification #

  • New tests in lib/runtime/web.test.ts mount mountBuiltAssets on public/. They check that the three fonts the site uses return 200 with font/woff2 and the cache header, that /favicon.svg still serves, and that a missing font falls through to 404 with no cache header.
  • I ran pnpm build, which copies the fonts into dist/fonts/. Then I started pnpm serve on a spare port with a dummy DB URL and fetched the fonts. /fonts/OverusedGrotesk-VF.woff2 and /fonts/JunicodeVF-Italic.woff2 returned 200 font/woff2 with Cache-Control: public, max-age=86400, and /fonts/nope.woff2 returned 404.
  • pnpm run check and pnpm run test pass (161 files, 2525 tests).

Acceptance criteria #

Decisions you can reverse #

  • I served /fonts/* instead of moving the fonts under /static/. Moving them would mean changing the URLs in _renderer.tsx, global.css.ts and an index test. lib/og/automation.ts also reads the TTFs from public/fonts.
  • Fonts are cached with Cache-Control: public, max-age=86400 (one day), not immutable. Their file names have no content hash, so a re-subset font reaches browsers without a rename.
  • The cache header is set in a middleware after next(), and only on a 200. It is not set in serveStatic's onFound, for the reason below.

Out-of-scope bug (not fixed) #

/static/* sets Cache-Control: public, max-age=31536000, immutable in serveStatic's onFound. With the installed hono 4.13 and @hono/node-server 2.1.5, that hook runs after the response is built, so the header is dropped. I checked this: a /static/favicons/... file comes back 200 with no Cache-Control. Hashed static assets are probably not getting the long-lived cache they are meant to have.

Review: https://tangled.org/exosphere.site/airglow/compare/main...loop/3my32eycidr22

loop:pushed run=contribute-20261023-221235-3my32eycidr22 sha=ee637defd80140b5050f825a153eda5dc3c579c6

Verdict: merge. The branch fixes the issue the first way the issue suggests. The production runtime now serves /fonts/* from ./dist, so the three woff2 files that _renderer.tsx and global.css.ts reference no longer 404. The check and the tests both pass.

What I checked:

  • I read the whole diff (lib/runtime/web.ts, lib/runtime/web.test.ts). The static mounts moved unchanged into mountBuiltAssets(app, root), which still runs only under import.meta.env.PROD with root ./dist. A new /fonts/* serveStatic was added. It is wrapped in a middleware that sets Cache-Control: public, max-age=86400 only on a 200, so a missing font falls through to 404 with no cache header. Nothing outside the issue changed.
  • Every /fonts/ reference in app/ (OverusedGrotesk-VF, JunicodeVF-Roman, JunicodeVF-Italic) has a matching file in public/fonts/, which Vite copies into dist/. The new tests mount the function on public/ and assert 200, font/woff2 and the cache header for all three fonts, plus a 404 for a missing font.
  • pnpm run check passes. pnpm run test passes: 161 files, 2525 tests.

The PR also points out that the existing /static/* onFound cache header seems to be dropped. That bug was already there and is outside this issue. It may deserve its own issue.

loop:verdict run=review-20261023-222109-3my32eycidr22 sha=ee637defd80140b5050f825a153eda5dc3c579c6 verdict=merge

Merged main into the branch: dc9262c650636b0b21335cd8b74e59a057615f48.

loop:carry from=ee637defd80140b5050f825a153eda5dc3c579c6 sha=dc9262c650636b0b21335cd8b74e59a057615f48

Merged onto main as 00de4072eabe5cd073e58df39b5ad9056727c089.

loop:merged sha=00de4072eabe5cd073e58df39b5ad9056727c089 head=dc9262c650636b0b21335cd8b74e59a057615f48

Sign up or Sign in to add to the discussion.