# The favicon no convention emits

> Next has no file convention that produces an .ico, so four sites wrote the same Buffer arithmetic. buildIco packs the container, and writeIcons keeps a generated-and-committed file from being dirtied by every commit.

`icon.tsx` and `icon.svg` go through the metadata pipeline and produce a
`<link>`. `ImageResponse` produces PNG. **No Next convention emits an ICO** — it
is a container format, an `ICONDIR` header with one entry per size and the
encoded images concatenated, and nothing in the chain assembles one.

So `favicon.ico` is generated and committed, which makes it the one build output
that lives in git. Four sites had each written the same forty lines to produce
it. This is those forty lines, once.

```js
import { buildIco, writeIcons } from "@goflag/og";

// Imported here rather than at the top, so `--check` never loads it.
const rasterise = async (svg) => (await import("sharp")).default(Buffer.from(svg)).png().toBuffer();

const status = await writeIcons({
  artefacts: [
    {
      path: "public/favicon.ico",
      render: async () =>
        buildIco(
          await Promise.all(
            [16, 32, 48].map(async (width) => ({ width, bytes: await rasterise(icon(width)) })),
          ),
        ),
    },
    { path: "public/apple-touch-icon.png", render: () => rasterise(icon(180)) },
  ],
  lock: ".favicon-fingerprint",
  fingerprintOf: [16, 32, 48, 180].map(icon),
  check: process.argv.includes("--check"),
});
```

## It rasterises nothing, and that is the decision

Packing already-encoded images is pure byte manipulation with no dependency. The
moment this knew what `sharp` was, the package would carry a native binary for
every consumer — and the friction that comes with one, in CI, in Docker, on
Alpine.

Your site rasterises instead, with the `sharp` it already has for Next's image
optimisation. Same contract as the fonts the card does not embed: the package
does not ship what your site already has.

## Why a generated file needs a guard

An artefact that is both generated **and** committed has a failure mode of its
own, and it was observed on three of the four sites this came from.

A pre-commit hook regenerates it every commit. `sharp` encodes the same pixels
into slightly different bytes across versions. So the file is dirtied by every
commit that touches anything, the noise gets committed, review learns to skip it
— and a real change to the icon arrives invisible in the same diff.

**So the fingerprint is over the inputs and never over the output's bytes.** A
`sharp` upgrade that re-compresses the same picture is not a change to your icon,
and `writeIcons` refuses to record it as one. Pass the drawings, or whatever else
the files are a function of; a colour, a size or a geometry change invalidates it
and a dependency bump does not.

| Status      | What happened                                            |
| ----------- | -------------------------------------------------------- |
| `written`   | The inputs moved and the files were rewritten            |
| `unchanged` | They match. Nothing was rendered and nothing was written |
| `stale`     | `check` only: the files exist and no longer match        |
| `absent`    | `check` only: at least one is not there at all           |

It returns rather than exits, because a library that called `process.exit` would
be deciding an exit code on behalf of a script that knows its own name and its
own remediation message.

## `--check` belongs in CI, not in a hook

<Callout type="tip" title="A hook that rewrites a file cannot fail a build.">
  A check that writes nothing can. That is the whole difference, and it is the reason the guard is
  worth having at all.
</Callout>

Put `--check` in CI and in your pre-commit hook, and never the write path. A hook
running the write path is worse than redundant: it writes **after** git has
snapshotted the index and stages nothing, so editing a theme token and committing
lands the new stylesheet with the old icons — and leaves the regenerated files
loose for the next `git add -A` to sweep into a diff about something else.

Because `render` is only called when something has to be rewritten, `--check`
rasterises nothing. Import `sharp` lazily and the check needs no native binary at
all, which is what lets it run on an Alpine image.

## The four calls

| Call                                                    | Returns                                                  |
| ------------------------------------------------------- | -------------------------------------------------------- |
| `buildIco(entries)`                                     | a `Uint8Array` — the container, packed from encoded PNGs |
| `writeIcons({ artefacts, lock, fingerprintOf, check })` | one of the four statuses above                           |
| `writeIco(path, entries, options)`                      | the same, for the single-`.ico` case                     |
| `fingerprint(inputs)`                                   | the digest, if you are guarding something of your own    |

`writeIco` is the shorthand: one path, the entries to pack into it, and the same
options. `writeIcons` is the one to reach for as soon as a site ships more than
the container — the PWA sizes, the apple-touch icon, a maskable variant — because
guarding four files with one fingerprint and guarding one of them is the same
call with a longer list.

`buildIco` takes `{ width, height?, bytes }`. `height` defaults to `width`, since
every icon these sites ship is square, and a dimension outside 1–256 is refused
rather than truncated — the format has one byte per side, and a container no
shell will read is not a file worth writing.

`Uint8Array` and not `Buffer`, deliberately: `Buffer` is a global `@types/node`
contributes, and a declaration file naming it fails to compile for a consumer
whose tsconfig does not pull those types in. A `Buffer` **is** a `Uint8Array`, so
`sharp`'s output goes straight in.

## The rule that asks for this

[`icons.ico.missing`](/docs/rules/icons.ico.missing) is a guideline rather than a
spec: no specification requires `/favicon.ico`, and modern browsers follow the
`<link>` a page declares. But feed readers, link unfurlers and older crawlers ask
the root blind, take the 404, and show nothing.

[`icons.sizes-mismatch`](/docs/rules/icons.sizes-mismatch) is the one that pays
for itself. A `.ico` carrying 16, 32 and 48 declared as `48x48` advertises a
third of itself, and that half-true declaration is the common shape rather than
the exotic one.
