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.
| Convention | File types | Valid locations |
|---|---|---|
| favicon | ico | app root only |
| icon | ico, jpg, jpeg, png, svg | anywhere in app |
| apple-icon | jpg, jpeg, png | anywhere 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.
app/
favicon.ico
icon.svg
apple-icon.pngThat 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.
<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.
app/
icon1.png
icon2.pngThe 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/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.
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.
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.icoin 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
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.
Frequently Asked Questions
Where does favicon.ico go in the App Router?
Can I generate a favicon with code?
How do I ship more than one icon size?
Why does my browser still show the old favicon?
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.
More in this topic
`generateMetadata` Explained with Real Examples
What generateMetadata does, when it runs, and how to use it for real routes: awaited params, deduplicated data fetching, extending parent metadata, and returning a 404 from metadata.
Canonical URLs in Next.js: `metadataBase`, `alternates.canonical`, and Dynamic Pages
How canonical URLs work in the Next.js App Router: setting metadataBase once, writing alternates.canonical per route, handling dynamic segments, and what happens when the base URL is missing.
Open Graph and Twitter Card Metadata in Next.js
How to write Open Graph and Twitter card metadata in the Next.js App Router: the openGraph and twitter fields, automatic card defaults, article tags, and image merge rules.