Shipping · 2 min read · 19 October 2026

The favicon OpenNext corrupted

A Next.js site on Cloudflare Workers served a broken favicon in production. The build had read a binary file as text and cached the damage as immutable.

A cartoon browser tab with a broken pixel icon under a magnifying glass, and a pipe turning neat bits into squiggles
U+FFFD
what the favicon's invalid bytes turned into
88 → 9 KB
favicon size after the same performance pass
1
rule now: binaries live in public/

01 · The setup

A creative agency's company-profile site, built on Next.js 15.5.

It ran on Cloudflare Workers through @opennextjs/cloudflare, with an R2 incremental cache and a long-lived regional cache in front. Fast, cheap, and as it turned out, very good at keeping a mistake.

02 · The root cause

The favicon was a prerendered route, and the prerendered body was read as UTF-8 text.

Every byte that isn't valid UTF-8 became the replacement character U+FFFD. The corrupted result was then cached as immutable, so every request got the broken file until the cache entry changed. Nothing in the code looked wrong.

03 · The fix

Keep binaries out of anything that might treat them as text.

  • favicons, images, fonts and PDFs go in public/, served as static assets
  • route handlers that return binary data are forced dynamic
  • on this stack, skip the icon.* and opengraph-image.* file conventions
// app/report/route.ts — a handler that returns binary data
export const dynamic = "force-dynamic";

04 · Verify with a hash

When an asset is wrong only in production, compare bytes, not screenshots.

Hash the file you shipped and the file the live URL returns. If they differ, something between your build and the browser changed it. The same pass also cut the favicon from 88 KB to 9 KB.

shasum -a 256 public/favicon.ico
curl -s https://example.com/favicon.ico | shasum -a 256

05 · Lead row first

The same build taught me a pattern for contact forms.

The endpoint validates the request, rate-limits per IP using a salted hash, writes the lead row, and only then sends the email with the PDF and updates the status. If the email provider is down, the lead still exists. Order the steps so the thing you can't lose is saved first.

06 · A ticker that doesn't jump

Small detail, same project.

The logo ticker slows down on hover by changing the Web Animations playbackRate, not the animation duration, so it never jumps position. It's off when the user prefers reduced motion.

Next storyThe cookie-free Supabase client