Favicons, App Icons, and `apple-icon` in the App Router

How Next.js turns favicon, icon, and apple-icon files into link tags: where each file is valid, what the rendered tags contain, how to ship several sizes, and how to generate an icon with code.

8 min read

Icons in the App Router are file conventions: put a favicon.ico, icon, or apple-icon file in the app directory and Next.js writes the matching link tags into the document head. Nothing is declared in metadata, and nothing goes in the public folder. The tags carry the size and MIME type read from the file itself.

The behavior below was verified with Next.js 16.3 in an App Router project, reading tags from a production build that uses the default caching model, with the Cache Components flag off. All three conventions are static file lookups, so they cost nothing at request time.

Where each app icon file is allowed

The three app icon conventions differ in what they accept and where they can live. Only one of them is restricted to the root.

ConventionFile typesValid locations
faviconicoapp root only
iconico, jpg, jpeg, png, svganywhere in app
apple-iconjpg, jpeg, pnganywhere in app

The favicon restriction is the one that trips people. If you need a per-section icon, use the icon convention, which works in any segment.

texttext
app/
  favicon.ico
  icon.svg
  apple-icon.png

That layout produces three link tags on every route: the classic icon, the modern icon, and the Apple touch icon. Each URL carries a content-based query string, which is how a changed file gets a new URL instead of a stale cached one.

What the tags actually contain

Next.js inspects each file and fills in the attributes, so the tags describe the real image rather than what you remembered to type.

htmlhtml
<link rel="icon" href="/favicon.ico?favicon.2vob68tjqpejf.ico" sizes="256x256" type="image/x-icon" />
<link rel="icon" href="/icon.svg?icon.3cuw2vl4ecy83.svg" sizes="any" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-icon.png?apple-icon.3omtb6opobefe.png" sizes="180x180" type="image/png" />

Two details are worth reading closely. A raster file reports its real pixel dimensions, while a vector icon gets sizes="any" when nothing fixed can be read from it. Some bundlers do resolve a concrete size from an SVG, so treat the exact value as something to read rather than predict.

The Apple icon renders as a different relation, which is why it needs its own file rather than another icon entry. Ship it at 180 by 180 pixels, the size iOS uses for home screen shortcuts.

None of these tags are something you write by hand. Adding a link element in a layout would duplicate what the convention already produced, and the Metadata API offers an icons field for the rare case that needs a relation these files do not cover.

Shipping several app icons

A single icon is fine for most sites. When you need several, add a number suffix and Next.js renders one tag per file.

texttext
app/
  icon1.png
  icon2.png

The rendered head now contains two icon links, sorted lexically by file name, each reporting the size read from its own image. A browser picks the closest match, so a small file for tabs and a larger one for bookmarks and app listings is a sensible pair.

Ordering follows the file names, not the sizes, so keep the numbering consistent with the sizes if you care about the order in the source.

Resist shipping a long list. Every entry is another request a browser may make, and a modern icon plus one Apple icon covers the places most visitors will see it. A vector icon removes the question entirely, since one file scales to every slot.

Generating an app icon with code

Swap the image for a module and the icon is rendered on the server instead. The same size and content type exports used by the social image conventions apply here.

App.tsxApp.tsx
// app/marketing/icon.tsx
import { ImageResponse } from 'next/og'
 
export const size = { width: 32, height: 32 }
export const contentType = 'image/png'
const box = { width: '100%', height: '100%', display: 'flex', background: '#000' }
 
export default function Icon() {
  return new ImageResponse(<div style={box}>A</div>, size)
}

The build lists /marketing/icon as its own prerendered route, and pages under that segment render an icon link pointing at it with the declared size and type. The image is generated once at build time, since nothing in it depends on the request.

A segment icon replaces the inherited one

Routes under that segment use the generated icon instead of the icon inherited from the app root. The favicon and Apple icon tags keep coming from the root, because they are separate relations.

There is one limit here. A favicon cannot be generated, so the module form works for icon and apple-icon only, and an .ico file remains the only way to fill the favicon slot.

Verifying and cache-busting

App icons fail in ways that look like caching problems, and the browser tab is the worst place to debug them. Read the tags from a production build instead.

bashbash
npx next build && npx next start
curl -s http://localhost:3000/ | grep -o '<link rel="[^"]*icon[^"]*"[^>]*>'

Correct tags with the sizes you expect means the files are wired up, and any remaining difference is your browser holding an old icon. The query string in each URL changes when the file changes, which is what lets a crawler or a fresh profile pick the new one up.

Manifest icons are a separate system. If your project ships a web app manifest, it declares its own icon list, and the tags described here do not replace it.

The same verification habit applies to the rest of your head tags, which the SEO checklist for the App Router walks through route by route.

Common mistakes

These app icon failures survive a local check and show up on someone else's device.

  • Putting favicon.ico in the public folder, which works by accident on some setups and skips the generated tag.
  • Trying to place a favicon in a nested segment, where the convention is not read at all.
  • Using a transparent PNG that vanishes on a dark browser theme, since the tab background is not yours to control.
  • Shipping only an icon file and expecting iOS home screens to use it, when the Apple relation needs its own file.
  • Judging a change by the browser tab rather than by the rendered link tags.
Rune AI

Rune AI

Key Insights

  • favicon.ico is valid only at the top level of the app directory.
  • icon accepts ico, jpg, jpeg, png, and svg files anywhere in the app tree.
  • apple-icon accepts jpg, jpeg, and png, and renders an apple-touch-icon tag.
  • Numbered file names such as icon1.png produce one link tag per file.
  • A module named icon or apple-icon generates the image and replaces inherited icon tags for that segment.
RunePowered by Rune AI

Frequently Asked Questions

Where does favicon.ico go in the App Router?

In the top level of the app directory, not in the public folder. It is the only icon convention restricted to the root segment.

Can I generate a favicon with code?

No. The favicon convention accepts only an .ico file. Generate an icon instead, which supports a module that returns an image.

How do I ship more than one icon size?

Add a number suffix to the file name, such as icon1.png and icon2.png. Next.js renders one link tag per file, sorted lexically, with the size read from each image.

Why does my browser still show the old favicon?

Browsers cache icons aggressively. Next.js appends a content-based query string to the icon URL, so a hard reload or a fresh profile usually shows the new one.

Conclusion

Icons in the App Router are files rather than markup. Put favicon.ico at the app root, add an icon file for the modern tags, add apple-icon for iOS home screens, and use a module when the icon should be generated. Then read the link tags in the rendered HTML to confirm what shipped.