Codex vs Claude

The Favicon Set Is Four Files: Cutting the Package an Agent Gives You

David Guzenburg/ / 10 min read

Twenty files, eighteen lines of head markup, and three of them are for a browser nobody has run since 2017.

toolingbuild systemsconventionsquality gates

Ask for a full favicon set and you will get the 2014 one

"Generate a complete favicon package" is a request an agent answers enthusiastically and badly. You get twenty files: six Apple touch icon sizes, four Android sizes, three Microsoft tile images, a browserconfig XML, a multi-resolution ICO, a manifest, and eighteen lines of markup in the <head>.

That set was correct once. It encodes a decade of device-specific workarounds, most of which target browsers and platforms that have since converged on simpler behaviour. An agent produces it because that is what the tutorials and generator sites still say, and those pages rank well precisely because they have been around since the era they describe.

The modern set is about four files. The argument for cutting it is not tidiness — it is that every file in the set is a thing that can 404, go stale, or disagree with the others.

What actually gets requested

Current browsers read the <link> elements in your head and pick the best match. An SVG icon, if offered, is preferred by desktop browsers because it scales to whatever the tab strip needs. iOS uses the Apple touch icon when a page is added to the home screen. Android and installable web apps read the icons declared in the web app manifest. And anything that ignores all of the above — older readers, some crawlers, some chat link unfurlers — requests /favicon.ico from the root, whether or not you declared it.

That last behaviour is the reason the ICO file survives. It is not requested because you declared it; it is requested because it is a convention older than the declaration mechanism.

The set worth shipping

<link rel="icon" href="/favicon.ico" sizes="32x32">
<link rel="icon" href="/icon.svg?v=3" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=3">
<link rel="manifest" href="/site.webmanifest?v=3">

Four lines. The ICO at the root for the implicit request, carrying a 32 pixel image. The SVG for everything that will take it. A single 180-pixel Apple touch icon, no longer one per device generation. A manifest declaring the two sizes an installable app needs, 192 and 512.

The ?v=3 is not decoration and is covered below.

An SVG favicon can follow the theme

The genuinely new capability, and the one most sets omit: an SVG icon is a document, so it can carry a media query and respond to the browser's colour scheme. A dark monochrome mark that becomes light in a dark tab strip, from one file, with no JavaScript and no second asset.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    .mark { fill: #0b0d10; }
    @media (prefers-color-scheme: dark) {
      .mark { fill: #f4f5f7; }
    }
  </style>
  <path class="mark" d="M6 24V8h4l6 9 6-9h4v16h-4v-9l-6 9-6-9v9z"/>
</svg>

Design the mark for 16 pixels, not for the 512-pixel version you are looking at while you work. At tab size a favicon has room for one shape and roughly two colours. A detailed logo reduced to 16 pixels becomes a smudge, which is why so many favicons are a single letter — that is a constraint, not a lack of ambition.

Generate from one source, in the build

Every file in the set should derive from the SVG, so that changing the mark changes all of them. A set assembled by hand drifts within a year, and the symptom is a home screen icon that is a version behind the tab.

#!/usr/bin/env bash
set -euo pipefail
SRC=assets/brand/icon.svg
OUT=public

cp "$SRC" "$OUT/icon.svg"

# Apple touch icons are composited on a solid background; transparency
# there renders as black on some iOS versions.
resvg --width 180 "$SRC" /tmp/at.png
magick /tmp/at.png -background '#ffffff' -alpha remove -alpha off \
       "$OUT/apple-touch-icon.png"

for s in 192 512; do
  resvg --width "$s" "$SRC" "$OUT/icon-$s.png"
done

resvg --width 32 "$SRC" /tmp/f32.png
resvg --width 16 "$SRC" /tmp/f16.png
magick /tmp/f16.png /tmp/f32.png "$OUT/favicon.ico"

oxipng -o4 --strip safe "$OUT"/*.png
echo "favicons built from $SRC"

The transparency removal on the Apple touch icon is the detail that catches people. iOS composites that image onto the home screen without honouring an alpha channel in some versions, so a transparent background arrives as a black square around your mark. Flatten it deliberately against your brand background.

Favicons are cached far more aggressively than you expect

This is the part that turns a five-minute task into an afternoon. Browsers cache favicons independently of ordinary page assets, for a long time, and in some cases keep serving the old one after a hard refresh clears everything else. You will change the icon, deploy, see the old one, assume the deploy failed, and redeploy.

The fix is to make the URL change when the icon changes — the ?v=3 above, or a content hash in the filename for everything except /favicon.ico, which has to stay at that path because the request for it is implicit.

Verify somewhere with no cache

Check a favicon change in a fresh private window, or with a direct request to the URL, before concluding anything about whether the deploy worked. Your own browser is the least reliable observer of this particular change.

The check that pays for itself

The failure mode with favicons is not a bad icon, it is a missing file: a path declared in the head that does not exist on the server, quietly returning your 404 page with an HTML content type where an image was expected. Nothing breaks visibly. The icon is simply absent, and absence looks like a design choice.

#!/usr/bin/env bash
# Every icon path in the head, plus the implicit root request, must
# return 200 with an image content type.
set -euo pipefail
BASE="${1:-http://localhost:8080}"
fail=0

paths=$(grep -oE 'href="/[^"]+\.(ico|svg|png|webmanifest)[^"]*"' public/index.html \
        | sed 's/href="//; s/"$//')

for p in $paths /favicon.ico; do
  read -r code type < <(curl -sSI "$BASE$p" \
    | awk 'tolower($1)=="http/1.1"||tolower($1)=="http/2"{c=$2}
           tolower($1)=="content-type:"{t=$2} END{print c, t}')
  case "$code:$type" in
    200:image/*|200:application/manifest*|200:text/xml*) ;;
    *) echo "$p -> $code $type" >&2; fail=1 ;;
  esac
done

exit $fail

Run it against a built preview in CI. It takes under a second, it catches the case where a path was renamed and the head was not updated, and it catches the more insidious case where the server returns 200 with an HTML error page — which a plain status check would pass.

The manifest is two fields you need and eleven you do not

Generated manifests arrive full of properties copied from a specification example: orientation locks, scope declarations, categories, screenshots, shortcuts, an ID. Almost none of it does anything for a site that is not an installable application.

What matters is the icon array with 192 and 512 entries, a name, and a theme colour if you have one. Everything else should be there because you decided it should. An orientation lock or a display: standalone that nobody chose changes real behaviour for anyone who installs the site, and it got there because a generator emitted a template.

The touch icon is a different design problem

The Apple touch icon is not a favicon at a larger size, and treating it as one produces the most common visual defect in a generated set.

It renders at roughly 60 device-independent pixels on a home screen, beside other application icons, with the corner radius applied by the operating system rather than by you. Which means: no transparency, no rounded corners of your own, and a mark sized to leave a margin, because a design that fills its square looks cramped next to icons that do not. A favicon mark scaled to 180 pixels and shipped unchanged reads as a sticker rather than an app.

The corner radius point is worth being explicit about. Baking rounded corners into the PNG produces a double-rounded icon on iOS: your radius, clipped again by the system's. It looks subtly wrong in a way that is hard to name and easy to see once you know.

Social preview images are not part of this set

Ask for favicons and an agent will often throw in Open Graph tags and a preview image, which is helpful and is a different job with different constraints. A social card is 1200 by 630, needs to be legible as a thumbnail in a feed, usually contains text, and is frequently per-page rather than per-site.

Keep them separate. The favicon pipeline generates from one mark and rarely changes. The social pipeline generates per page, often at request time, usually from a template with the page title composited in. Merging them produces a script that does neither well and a preview card that is your logo on a white background, which conveys nothing in a feed.

Monochrome and mask icons

Older sets include a Safari pinned-tab mask icon: a single-colour SVG with a specific path convention. Current Safari versions read the standard SVG icon instead, so the mask icon is one of the files worth dropping unless you support a browser version old enough to need it.

Its replacement in the modern set is the monochrome purpose in the web app manifest, used for adaptive icons that the platform tints. If you support installation and the platform themes your icon, providing a monochrome variant means it is legible after tinting rather than becoming a solid blob. That is one extra manifest entry and one extra file, and it applies only if installation matters to you.

The whole thing as a checklist

One mark, authored as SVG, designed to read at 16 pixels. Four declarations in the head. An ICO at the site root because the request is implicit. An SVG icon carrying a prefers-color-scheme rule. One 180-pixel Apple touch icon, flattened against a solid colour, with system-side corner rounding, and internal margin so the mark does not fill the square. A manifest with 192 and 512, plus a monochrome variant if installation matters.

All of it generated from the single source file in the build. Versioned URLs so a change is actually visible. A CI check that every declared path returns an image and not an HTML error page. Verified in a private window, because your own browser is the last thing to notice.

That is the complete job, and it is roughly a quarter of what "generate a full favicon package" produces. The excess is not merely wasted — every extra file is a path that can rot, a size that can drift out of sync with the mark, and a line in the head asserting something that may no longer be true.

Why the obsolete package persists

Worth naming, because it explains a whole category of agent output. The twenty-file favicon set is not a hallucination; it is an accurate summary of what the highest-ranking pages on this topic say. Those pages rank because they are old and heavily linked, and they are old because the topic seemed settled in 2015.

An agent asked a question whose best public answer is a decade stale will give you the decade-stale answer, confidently, with correct-looking markup. That is not a failure of reasoning — it is what the corpus contains. The lesson generalises well past favicons: for any question where the practice has quietly moved on but the tutorials have not, expect the tutorial, and check against the platform's own documentation rather than the top result.

The favicon is also a useful calibration exercise, because it is small enough to actually finish. Four files, one source, one build step, one check. If that takes an afternoon rather than ten minutes, the friction you hit — no single source of truth for the mark, no build step to hang generation off, no CI job that can fetch a URL — is the same friction that makes every larger asset problem expensive, and you have just found it cheaply.

Takeaway

The twenty-file favicon package an agent produces is a decade of obsolete device workarounds. Ship four: an ICO at the root for the implicit request, an SVG that follows prefers-color-scheme, one 180-pixel Apple touch icon flattened against a solid background, and a manifest with 192 and 512. Generate all of them from one source in the build, version the URLs because favicon caching outlives a hard refresh, and check in CI that every declared path returns an image rather than your 404 page.

Keep reading
Codex vs Claude

Generated Placeholder Assets: Borrowing Against a Design You Have Not Made

Agent-generated placeholder icons unblock a build and quietly ship to production. Path conventions no production file may import, a four-line CI gate, watermarks, and the provenance record that makes the licensing question answerable.

Codex vs Claude

Optimising SVG: The Byte Count Reports the Saving, Not the Damage

Default optimiser settings strip viewBox, mangle referenced ids, merge animated paths and round flush edges apart. A safe checked-in configuration, and verifying by rendered pixels rather than file size.

Codex vs Claude

Constructing SVG Icons in Code: viewBox, currentColor and the Rest Is Detail

What to check first in agent-written SVG: the coordinate contract, colour inheritance, non-scaling strokes, the labelled-versus-decorative decision, and a twenty-line linter that replaces the review comment you keep repeating.

Codex vs Claude

One Agent, Many Surfaces: They Share a Model and Nothing Else

Terminal, editor, desktop, browser and hosted runner are different tools sharing an account. What differs, why consistency has to come from the repository, verifying permission denials per surface, and choosing a surface by the shape of the task.

← Fewer Tool Calls: Efficiency or Missing Evidence?  ·  Interrupting an Agent: Almost Everything Is in the First Minute →

All codex vs claude articles  ·  Every article