Medium wave · 540–1600 kHz81 stn
1567kHz

Your Favicon Markup Is Mostly Dead Code

A dark steampunk vault where one small brass emblem glows teal and purple on a pedestal, while behind it a towering wall of dusty identical copper stamping dies of every size sits unused under cobwebs and drifting fog, reflected in a wet iron floor

View source on almost any site that was set up more than a couple of years ago and scroll to the top of the head. Before the title, before the stylesheet, there’s a block of fifteen or twenty lines that nobody on the team has read since the day it was pasted in: favicon-16x16.png, favicon-32x32.png, android-chrome-192x192.png, mstile-150x150.png, a safari-pinned-tab.svg with a hex color, a browserconfig.xml, and a msapplication-TileColor meta tag pointing at a Windows Start screen that no longer exists.

That block came out of a favicon generator, and it was reasonable when it was generated. Most of it has been dead weight ever since. Every page you ship carries it, and every one of those files is something you have to regenerate the next time the logo changes.

Modern browsers need far less. Here’s the whole thing.


The complete set

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

Four lines. Behind them sit a small number of files:

FileSizeWho reads it
favicon.ico32x32Browsers without SVG favicon support, crawlers, tools
icon.svgVectorModern browser tabs, bookmarks, history
apple-touch-icon.png180x180iOS and iPadOS home screen
icon-192.png192x192Android home screen, install prompts
icon-512.png512x512Splash screens, app drawers
icon-mask.png (optional)512x512Android adaptive icons via purpose: maskable

The last three are referenced from the manifest, not the HTML. That’s the entire surface area, and it covers every browser and platform a content site realistically ships to.


Why each of the old files can go

The PNG ladder

favicon-16x16.png and favicon-32x32.png existed because browser tabs needed raster icons at specific sizes and some browsers ignored ICO. An SVG renders crisply at every size a tab, bookmark bar, or history list asks for, and the ICO covers the browsers that don’t take SVG. The two intermediate PNGs are serving nobody.

Windows tiles

browserconfig.xml, mstile-*.png, and the msapplication-* meta tags configured pinned-site tiles for Internet Explorer and the original Edge on Windows 8 and 10. Both browsers are gone. Chromium-based Edge reads the same icons and manifest as Chrome. These files are now requested by nothing except the occasional scanner looking for old IIS installs.

The Safari mask icon

safari-pinned-tab.svg with rel="mask-icon" was a monochrome silhouette for Safari’s pinned tabs, back when Safari showed no favicons in its tab bar at all. Safari has displayed regular favicons for years now, and pinned tabs fall back to them. The mask icon is a nicety at best, and one more file to keep in sync with the logo.

The precomposed touch icon

apple-touch-icon-precomposed.png told old iOS versions not to add the glossy highlight effect to your home screen icon. iOS stopped adding the gloss back in iOS 7. One 180x180 apple-touch-icon.png is all modern iPhones and iPads use; they scale it for every other context.


The details that actually matter

Trimming the list is the easy part. The four files that remain each have one rule that generators routinely get wrong.

Keep favicon.ico at the root, no matter what

Browsers, feed readers, link-preview bots, and half the SEO tools on the internet request /favicon.ico directly, without reading your HTML. If it’s missing, every one of those requests is a 404 in your logs. On a framework with a catch-all dynamic route, it’s worse: the 404 can render your entire error page, layout and all, just to tell a crawler there’s no icon.

So the ICO stays, at the root, under that exact name. It doesn’t need to be the multi-resolution monster of old. A single 32x32 image is enough. ICO files can even wrap a PNG directly, which every browser since the Vista era understands.

The sizes="32x32" attribute on its link tag is not decoration. Without it, some Chromium versions prefer the ICO over the SVG when both are declared. Telling the browser the ICO is a fixed 32x32 bitmap lets it pick the vector.

Let the SVG follow the theme

An SVG favicon can carry its own stylesheet, including a media query. That gives you a dark-mode favicon with zero extra files:

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
  <style>
    path { fill: #1a1a2e; }
    @media (prefers-color-scheme: dark) {
      path { fill: #e0e0ff; }
    }
  </style>
  <path d="M4 4h24v24H4z"/>
</svg>

The media query follows the operating system or browser color scheme, not your site’s theme toggle. For a tab icon, that’s the correct behavior: the icon needs to contrast with the browser chrome it sits in, and the browser chrome follows the OS.

Run the SVG through an optimizer before shipping it. A logo exported from a design tool often carries editor metadata, unused definitions, and decimals to six places. A favicon should be a few hundred bytes.

Give the touch icon a solid background

iOS does not respect transparency on home screen icons. Transparent pixels become black, so a dark logo on a transparent background turns into a black square on a phone. The apple-touch-icon.png should be fully opaque, with your brand background baked in and roughly 20 pixels of padding around the mark so it survives iOS’s rounded-corner mask.

Like the ICO, iOS requests /apple-touch-icon.png at the root even when no tag declares it. Put it there and the link tag becomes a formality.

Make the maskable icon actually maskable

Android launchers crop adaptive icons into circles, squircles, or rounded squares depending on the device. An icon marked purpose: maskable promises that everything important sits inside the safe zone, a circle whose diameter is 80 percent of the image. A logo that fills the full 512x512 canvas will have its corners sheared off.

{
  "name": "Static Signal",
  "short_name": "Signal",
  "icons": [
    { "src": "/icon-192.png", "type": "image/png", "sizes": "192x192" },
    { "src": "/icon-512.png", "type": "image/png", "sizes": "512x512" },
    { "src": "/icon-mask.png", "type": "image/png", "sizes": "512x512", "purpose": "maskable" }
  ],
  "theme_color": "#1a1a2e",
  "background_color": "#1a1a2e",
  "display": "standalone"
}

Don’t mark your regular icon as "purpose": "any maskable" to save a file. A padded icon looks shrunken in contexts that don’t mask, and an unpadded one gets cropped in contexts that do. They’re different images for a reason.


Generate them from one source

Most of the rot in favicon markup comes from the files being generated once, by a web tool, and never again. Make them a build artifact instead. If your site already uses Astro’s default image service, sharp is already in your dependency tree, and it rasterizes SVG directly:

// scripts/icons.mjs
import { readFile, writeFile } from 'node:fs/promises';
import sharp from 'sharp';

const src = await readFile('src/icons/logo.svg');
const bg = { r: 26, g: 26, b: 46, alpha: 1 };
const raster = (size) => sharp(src, { density: 384 }).resize(size, size);

// Padded and opaque: iOS fills transparency with black.
const padded = (size, inner) =>
  raster(inner)
    .flatten({ background: bg })
    .extend({ top: (size - inner) / 2, bottom: (size - inner) / 2, left: (size - inner) / 2, right: (size - inner) / 2, background: bg })
    .png()
    .toBuffer();

await writeFile('public/apple-touch-icon.png', await padded(180, 140));
await writeFile('public/icon-192.png', await raster(192).png().toBuffer());
await writeFile('public/icon-512.png', await raster(512).png().toBuffer());
await writeFile('public/icon-mask.png', await padded(512, 320));

// A single-image ICO is a 6-byte header, a 16-byte entry, then the PNG.
const png = await raster(32).png().toBuffer();
const ico = Buffer.alloc(22);
ico.writeUInt16LE(1, 2); // type: icon
ico.writeUInt16LE(1, 4); // one image
ico.writeUInt8(32, 6); // width
ico.writeUInt8(32, 7); // height
ico.writeUInt16LE(1, 10); // color planes
ico.writeUInt16LE(32, 12); // bits per pixel
ico.writeUInt32LE(png.length, 14);
ico.writeUInt32LE(22, 18); // image data offset
await writeFile('public/favicon.ico', Buffer.concat([ico, png]));

That’s every raster icon from one SVG, no favicon package required. Copy the optimized SVG itself to public/icon.svg and you’re done. Wire the script into a prebuild step or run it when the logo changes; either way, the source of truth is one file in your repo instead of a ZIP from a website.


Favicons cache harder than anything else

One last trap, because it bites everyone during a rebrand. Browsers keep favicons in a dedicated cache that is separate from the normal HTTP cache and notoriously sticky. Hard refreshes often don’t clear it. Clearing site data sometimes doesn’t either. Users can see the old icon in their bookmarks and history for weeks.

The reliable fix is a new URL. You can’t rename /favicon.ico, because tools request it by name, but you can version everything that’s declared in markup:

<link rel="icon" href="/icon.svg?v=2" type="image/svg+xml">
<link rel="apple-touch-icon" href="/apple-touch-icon.png?v=2">

And because these files sit at stable names in public/, they fall into the stable-named bucket of your caching config, not the hashed one. Give them a short or revalidating Cache-Control, not a year-long immutable, or the next logo change will be stuck behind a cache you told the browser it could trust forever.


The takeaway

Open your head and count the icon-related lines. If there are more than four, you’re carrying a generator’s assumptions about browsers from another decade. Delete the tiles, the mask icon, the PNG ladder, and the precomposed touch icon. Keep one ICO at the root, one SVG that follows the color scheme, one opaque touch icon, and a manifest with honest maskable art. Then generate all of it from one SVG, so the next time the logo changes it’s a build, not an archaeology project.