Back to News & Insights
Web Development October 14, 2026 · 5 min read

Our favicon is 1,322 bytes, our touch icon is rasterised from it at build time, and we serve no favicon.ico at all

One SVG, three outputs, and the Next.js metadata rule that silently gives your share card to the homepage and to nothing else.

Our favicon is 1,322 bytes, our touch icon is rasterised from it at build time, and we serve no favicon.ico at all

Munchable is a Next.js site with a bitten chocolate chip cookie for a logo. There are three places a browser or a crawler asks for that logo, and the three answers are built differently on purpose. You can check every number in this post yourself with curl.

1,322 bytes, a 32-unit viewBox, four large chips in a single tone. It is a deliberate simplification of the master artwork, which has seven chips in two browns, because at 16 pixels seven chips in two shades is mud. That is optical work, not a port, and it is the only reason two drawings of the same cookie are allowed to exist in the repo.

favicon.ico is a container format holding several bitmap sizes, and the thing it exists for is a browser that cannot read rel="icon" with an SVG. Every browser we serve can. Shipping one would mean another artefact to regenerate every time the mark changes, to be subtly wrong the one time somebody forgets. A 404 on a path nobody fetches costs nothing.

Home screen icons are the exception. iOS ignores SVG for apple-touch-icon, so that one has to be a PNG, and a PNG of a logo is exactly the sort of file that drifts: somebody exports it once at 180 pixels, the mark gets tweaked a year later, and the home screen keeps the old cookie forever.

The useful trick is the data URI. Satori, the renderer behind next/og, will rasterise an SVG handed to it that way, with its masks intact. So the touch icon is not an approximation of the favicon, it is the favicon, on the cream background, with 20 points of breathing room on each side because iOS rounds the corners and a full bleed mark loses its rim.

The 1200x630 card every shared link renders was, for a while, 141 lines of JSX in app/opengraph-image.tsx: the site's warm gradient, two faint radial blobs mostly off canvas, four crumbs hugging the edges, the cookie twice (46 pixels beside the wordmark, 340 pixels as the thing your eye lands on), and three font files read off disk and handed to ImageResponse.

Its own comment made the case for itself: generated rather than checked in as a binary, so the palette lives in one place and the card cannot drift away from it.

It is now a 122,886-byte PNG checked into the repository, and I think that was the right trade.

A share card is not a page. It changes when the brand changes, which is roughly never, and in between it must produce byte-identical output on every build. A build-time render is a strange way to guarantee that: it is three TTFs, a renderer with its own flexbox subset, and a layout you cannot see without building. The guarantee it offers (this card cannot drift from the palette) is real, but it is a guarantee against a change you would make deliberately, once, and would notice.

The touch icon is the opposite case and that is why it is still generated: it is the same artwork at a different resolution, it must follow the source exactly, and nobody would ever notice if it stopped.

Here is the part that is actually a Next.js gotcha, and left alone it would give this site exactly one share card.

The opengraph-image file convention fills in openGraph.images for you. It only does that for a segment that does not set openGraph itself. Every content page on this site sets its own openGraph, because each one needs its own og:url, title and description. So for all of them the convention does not apply: the homepage would have a card, and the other 425 URLs in the sitemap would have none.

The fix is to name the image in the shared metadata helper rather than rely on the convention:

The alt line is a second trap sitting inside the first. The file convention also supplies alt text, from opengraph-image.alt.txt or an exported alt. Set openGraph.images yourself and that goes too, so og:image:alt is emitted nowhere unless you restate it. There is now one copy of that sentence in the metadata helper and one in opengraph-image.alt.txt, which is the kind of duplication that is tolerable precisely because the alt text of a share card does not change.

That is the congee recipe; the same check works on the reflux condition page and on any of the 373 generated pages under the answers index.

The answer pages live at the site root: app/[question]/page.tsx serves /is-apple-low-fodmap and 372 others. A dynamic segment at the root means every top-level asset path is in the same namespace as the content, so the content module keeps a reserved set, checked at module load, of paths a generated slug may never take. apple-icon is in it. So is opengraph-image.png.

Which is why renaming that route from /opengraph-image to /opengraph-image.png was a two-file change: the metadata helper, and the reserved list that stops a page from being shadowed by an image.

No slug could collide today, since all of them begin is- or does-. The list is there so that adding a new phrasing in a year cannot quietly delete a page.

Want to discuss this further?

Book a free strategy call with our team to see how these insights apply to your specific business goals.

Book a consultation