For a new App Router project, the simplest setup is to place favicon.ico, icon.png, and apple-icon.png in the root app directory. Next.js detects those reserved filenames and generates the <link> elements for you.
app/
├── apple-icon.png
├── favicon.ico
├── icon.png
├── layout.tsx
└── page.tsx
You do not need to import these files or write manual head markup. Run a production build, open the rendered page, and verify the generated links and their responses.
That is the short answer. The right production choice depends on whether you need a full cross-platform pack, nested-route branding, code-generated artwork, or the older Pages Router.
This guide is for App Router projects on Next.js 13.3 and later, with a separate section for the Pages Router. If you are editing a framework-free document, use the HTML installation guide instead of translating Next.js metadata code back into manual tags.
Choose the setup before copying files
| Need | Best option | Why |
|---|---|---|
| One global icon with minimal code | App Router file conventions | Next discovers and describes the files automatically. |
A complete pack under /public/favicons | Metadata API | You control sizes, types, manifest, and paths explicitly. |
| An icon rendered from code or tenant data | app/icon.tsx or apple-icon.tsx | ImageResponse can generate image bytes at build or request time. |
| Different icons for route segments | Nested icon.* files | Metadata follows the App Router segment hierarchy. |
A project using pages/, not app/ | next/head in the Pages Router | The App Router metadata conventions do not apply. |
Do not implement all four “just in case.” Duplicate declarations make cache debugging and browser selection harder. Pick one primary system, then add only the fallbacks your platform coverage requires.
Method 1: use App Router file conventions
The official icon file-convention reference recognizes these static files:
app/favicon.icoapp/icon.ico,app/icon.jpg,app/icon.jpeg,app/icon.png, orapp/icon.svgapp/apple-icon.jpg,app/apple-icon.jpeg, orapp/apple-icon.png
There is one important asymmetry: favicon.ico is only supported at the top level of app/. icon and apple-icon files can live farther down the route tree.
Recommended global file set
src/
└── app/
├── apple-icon.png # 180x180 is a common Apple touch size
├── favicon.ico # multi-size legacy/browser fallback
├── icon.png # square browser and search icon
├── layout.tsx
└── page.tsx
If your project does not use src/, put the same files under the root app/ folder. Do not also copy them into public/ unless another declaration deliberately references those copies.
Next.js reads image metadata and emits tags similar to these:
<link rel="shortcut icon" href="/favicon.ico?..." />
<link rel="icon" href="/icon.png?..." type="image/png" sizes="64x64" />
<link rel="apple-touch-icon" href="/apple-icon.png?..." type="image/png" sizes="180x180" />
The exact generated URL and attributes are framework output; inspect your own build rather than hard-coding the example. The static file convention is especially useful because image dimensions become metadata automatically.
Multiple static browser icons
Numbered filenames let you provide more than one candidate:
app/
├── icon1.png
├── icon2.svg
└── icon3.ico
Use this only when the formats serve a real purpose. A multi-resolution ICO plus one modern PNG or SVG is usually easier to reason about than a pile of near-identical PNGs.
Per-section branding
An icon in a nested segment applies within that metadata segment:
app/
├── favicon.ico
├── icon.png
├── layout.tsx
├── page.tsx
└── dashboard/
├── icon.png
├── layout.tsx
└── page.tsx
This is useful for genuinely separate products or tenant areas. It does not give Google Search a different icon for every folder on one hostname: Google's Search documentation supports one favicon per hostname. Use route-level icons for the browser experience, not as a promise of path-level Search branding.
A short visual walkthrough of the App Router file placement. The framework evolves, so use the current documentation and your generated HTML as the source of truth.
Watch on YouTubeMethod 2: declare a full pack with the Metadata API
Use this method when a generator gives you named files, a web manifest, and several intentional sizes. Put browser-fetchable assets under public/:
public/
└── favicons/
├── apple-touch-icon.png
├── favicon-16x16.png
├── favicon-32x32.png
├── favicon-48x48.png
├── favicon-64x64.png
├── favicon-96x96.png
├── favicon-128x128.png
├── favicon-256x256.png
├── favicon-512x512.png
├── favicon.ico
└── site.webmanifest
Files in public/favicons/ are requested from /favicons/, without the word public. This follows Next.js's public folder convention.
Declare the pack in the root layout:
import type { Metadata, Viewport } from 'next';
export const metadata: Metadata = {
manifest: '/favicons/site.webmanifest',
icons: {
icon: [
{ url: '/favicons/favicon.ico', sizes: 'any' },
{ url: '/favicons/favicon-16x16.png', type: 'image/png', sizes: '16x16' },
{ url: '/favicons/favicon-32x32.png', type: 'image/png', sizes: '32x32' },
{ url: '/favicons/favicon-48x48.png', type: 'image/png', sizes: '48x48' },
{ url: '/favicons/favicon-64x64.png', type: 'image/png', sizes: '64x64' },
{ url: '/favicons/favicon-96x96.png', type: 'image/png', sizes: '96x96' },
{ url: '/favicons/favicon-128x128.png', type: 'image/png', sizes: '128x128' },
{ url: '/favicons/favicon-256x256.png', type: 'image/png', sizes: '256x256' },
{ url: '/favicons/favicon-512x512.png', type: 'image/png', sizes: '512x512' },
],
apple: [{ url: '/favicons/apple-touch-icon.png', type: 'image/png', sizes: '180x180' }],
},
};
export const viewport: Viewport = {
themeColor: '#ffffff',
};
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode }>) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}
Use the viewport export for themeColor; it is no longer a general metadata field. Keep the hex value aligned with your app chrome and manifest.
A practical web manifest
{
"name": "Example App",
"short_name": "Example",
"icons": [
{
"src": "/favicons/favicon-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/favicons/favicon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
A manifest serves installed web-app surfaces; it does not replace rel="icon". The MDN manifest icon reference documents sizes, type, and purpose.
If your generated pack uses 192px instead of the 128px or 256px examples above, declare the files you actually have. Never describe a nonexistent size in metadata.
Method 3: generate an icon with code
Create app/icon.tsx when the icon needs text, colors, or artwork produced by code:
import { ImageResponse } from 'next/og';
export const size = { width: 64, height: 64 };
export const contentType = 'image/png';
export default function Icon() {
return new ImageResponse(
<div
style={{
alignItems: 'center',
background: '#0f766e',
color: '#ffffff',
display: 'flex',
fontSize: 38,
fontWeight: 800,
height: '100%',
justifyContent: 'center',
width: '100%',
}}
>
E
</div>,
size
);
}
Next.js generates the image and its metadata route. According to the file-convention docs, generated icons are statically optimized by default unless the code uses dynamic APIs or uncached data.
Constraints worth knowing:
- Generate
iconorapple-icon;faviconcannot be generated this way. - The output must fit the underlying
ImageResponsesize limit documented by Next.js. - Remote fonts or images add failure modes; bundle or fetch them deliberately.
- Dynamic tenant data can turn an otherwise static icon into request-time work.
- Always inspect the route response after
next build, not just in development.
For several programmatic variants, use generateImageMetadata to return IDs and sizes, then render the requested variant in the default icon function.
Pages Router setup
If the application uses pages/ and has no app/ directory, put the pack in public/favicons/ and declare it through next/head in pages/_app.tsx:
import type { AppProps } from 'next/app';
import Head from 'next/head';
export default function App({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<link rel="icon" href="/favicons/favicon.ico" sizes="any" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicons/favicon-32x32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/favicons/apple-touch-icon.png" />
<link rel="manifest" href="/favicons/site.webmanifest" />
</Head>
<Component {...pageProps} />
</>
);
}
Do not carry this next/head pattern into an App Router layout. Use metadata there. Mixing the APIs is a common source of duplicate or unexpectedly ordered tags.
What our production-style test found
We used the Metadata API configuration above in a real Next.js 16.0.10 app, built the page, and requested each emitted URL on July 19, 2026.
| Output group | Emitted links | Result |
|---|---|---|
| Manifest | 1 | HTTP 200, manifest content type |
| ICO browser fallback | 1 | HTTP 200, icon content type |
| PNG browser icons | 8 | HTTP 200, image/png |
| Apple touch icon | 1 | HTTP 200, image/png |
| Total | 11 | All targets reachable |
The useful lesson was not the count. It was that a correct TypeScript object does not prove the assets were copied into the deployment. Checking every emitted href caught that whole class of failure.
Verify the build, not only next dev
Run the production path locally:
npm run build
npm run start
Then verify in this order:
- Open the page and inspect
<head>in DevTools. - Count icon and manifest links; remove unintended duplicates.
- Open every
hrefdirectly. - Confirm HTTP 200 and the expected
Content-Type. - Inspect intrinsic width and height for PNGs.
- Reload with DevTools cache disabled.
- Run the public URL through the Favicon Checker.
Useful terminal checks:
curl -Ls http://localhost:3000/ | grep -oE '<link[^>]+(icon|manifest)[^>]*>'
curl -sS -D - -o /dev/null http://localhost:3000/favicons/favicon-64x64.png
curl -sS http://localhost:3000/favicons/site.webmanifest
Common failures and exact fixes
The file is in public/, but the URL is 404
Remove /public from the URL. public/favicons/icon.png is served as /favicons/icon.png.
Also check filename case. macOS can hide a mismatch such as Favicon.png versus favicon.png; a Linux deployment commonly will not.
app/favicon.png does nothing
favicon is reserved specifically as favicon.ico. Rename a PNG to icon.png, or use a real favicon.ico.
The icon works locally but not after export or deploy
Inspect the deployment artifact and the public URL. A Docker copy stage, monorepo root, ignore rule, or static-host publish directory can omit public/ even when development works.
The browser shows the old image
First open the icon URL directly and verify its pixels. If the server is current, test a private browser profile and disable cache in DevTools. During a brand migration, a new filename can prove selection, but keep the final search-facing URL stable once the migration is complete.
The favicon cache-buster tool helps distinguish cached responses from incorrect markup.
Two or more icon tags point to different logos
Search for every source of metadata:
find app pages -type f \( -name 'layout.*' -o -name '_app.*' -o -name '_document.*' -o -name 'icon.*' -o -name 'favicon.*' \)
Look for a static convention file, an icons metadata field, next/head, and third-party layout code. Remove the system you no longer use.
An icon under a nested route does not affect Google Search
It can affect that browser route, but Google supports one favicon per hostname. Use a subdomain if a separately indexed product needs distinct Search branding.
A base path changes the asset location
File-convention metadata is framework-managed. Explicit /favicons/... URLs in your Metadata API point to the host root. If the site is mounted under a base path, verify the actual deployed URL and include that base path in explicit public-file URLs where required. Never assume the development root matches the production mount.
Release checklist
- Choose file conventions, Metadata API, or generated icons as the primary system.
- Use a real square source image with legible small-size artwork.
- Keep
favicon.icoat the top-levelapp/if using that convention. - Reference
public/assets without/publicin the URL. - Declare only sizes that exist.
- Keep manifest paths absolute from the deployed site root or mount point.
- Use
viewportforthemeColor. - Run
next buildand the production server. - Inspect emitted tags and request every target.
- Check mobile saved-icon and installed-app surfaces when they matter.
- Audit the public domain after the deploy.
Frequently asked questions
Is app/favicon.ico enough?
It is enough for a basic browser icon. Add icon.png or SVG for a modern source, apple-icon.png for iOS saved shortcuts, and a manifest with suitable images when the site is installable.
Should the files go in app/ or public/?
Use app/ for reserved file conventions. Use public/ when you want stable, explicit URLs controlled by the Metadata API or Pages Router markup. Both work; accidental duplication is the problem.
What size should app/icon.png be?
Use a square source comfortably above small UI sizes, such as 64x64 or larger, and preview it at 16x16. For a complete multi-platform pack, include purpose-built sizes rather than relying on every consumer to resample one file.
Can I use an animated favicon?
Some browsers can display animated formats, but behavior and accessibility vary, and Search needs representative, stable branding. A static icon is the dependable default.
Why does Next.js append a query string to a convention icon?
The framework manages generated metadata URLs and cache invalidation. Do not copy a build-specific URL into source code. Declare the convention file and let Next.js emit the current link.
Does the App Router need a manual <head> element?
No. Use static metadata files, the metadata export, or generateMetadata. The framework composes the final head.
Primary references
- Next.js: Metadata icon file conventions
- Next.js: Metadata and generated metadata files
- Next.js:
metadataAPI - Next.js: Public folder
- MDN: Web app manifest
icons
For files and tags outside the framework abstraction, continue with how to add the pack to HTML. It makes the path-resolution and response-header checks explicit.
