How to properly add a favicon in Next.js (App Router)
Most favicon tutorials for Next.js still tell you to paste a block of <link> tags into a layout or a custom document. In the App Router there is a better way: name the files correctly, put them at the top level of app/, and Next.js generates the tags on every page for you. No JSX <head>, no next/head, and nothing to repeat in your root layout.
This guide covers that file-convention route first, because it is the right default for a new App Router site. It then shows the metadata.icons alternative for the cases the conventions cannot express — extra sizes, light and dark variants, or icons that already live in public/ — and the mistakes that make a correctly drawn icon silently not appear.
The short answer: files in app/, manifest icons in public/
For a site-wide favicon, the finished project looks like this:
app/
favicon.ico
icon.svg
icon.png
apple-icon.png
manifest.webmanifest
public/
web-app-manifest-192x192.png
web-app-manifest-512x512.png
If your project uses the src directory, the same files go in src/app/ instead. That is the whole integration: the favicon generator for Next.js names a downloaded package exactly this way, so you can extract it at the project root, drop the convention files into app/ and the two manifest PNGs into public/, and build. Do not rename the files back to plain-HTML names such as favicon.svg or apple-touch-icon.png inside app/ — under the App Router those names are just files Next.js does not recognise as metadata.
What each file convention does
File in app/ | Becomes | Notes |
|---|---|---|
favicon.ico | rel="icon" (ICO) | Only recognised at the top level of app/, and only as .ico. Use a multi-size ICO containing 16×16, 32×32 and 48×48. |
icon.svg | rel="icon" type="image/svg+xml" | The main desktop tab icon: sharp at every size, and the only file here that can adapt to dark mode from inside the SVG. |
icon.png (also .jpg/.jpeg) | rel="icon" (raster) | The fallback browsers and Google can use when SVG is unsuitable. A 96×96 PNG meets Google’s guidance (square, larger than 48×48, a supported raster format). |
apple-icon.png (also .jpg/.jpeg) | rel="apple-touch-icon" | Provide it at 180×180 and fully opaque; iOS fills transparency with black and applies its own corner mask. |
manifest.webmanifest or manifest.json | rel="manifest" | A static Web App Manifest. Its 192×192 and 512×512 icons are regular static files, so they live in public/ and are referenced by URL. |
You may ship icon.svg and icon.png together: Next.js emits a tag for each and the browser picks the first format it supports, so the SVG leads on modern desktop browsers while the PNG remains as the safety net. Next.js also supports generated icons — an app/icon.tsx (or .js/.jsx) that returns an ImageResponse from next/og, and the equivalent apple-icon.tsx. That is useful for a dynamic lettermark, but for a real logo a static package is simpler, faster and covers sizes a single generated 32×32 icon does not.
Step by step
- Generate a complete package, not a single ICO. Start from a square master of at least 512×512 (or your SVG logo) and produce the files in the table above. The Next.js package does this renaming for you and previews the icon in light and dark tabs and in Google results before you download.
- Put the convention files at the top level of
app/. Site-wide icons belong next to your rootlayout.tsx, not inpublic/and not in a route folder. Only the two manifest PNGs (and any legacy extras) go inpublic/. - Check the manifest paths. A static
app/manifest.webmanifestis plain JSON whose icon URLs point atpublic/, for example:
{
"name": "Your site name",
"icons": [
{ "src": "/web-app-manifest-192x192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" },
{ "src": "/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" },
{ "src": "/web-app-manifest-512x512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" }
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
Prefer to build the manifest in code? Replace the static file with an app/manifest.ts that returns a MetadataRoute.Manifest object — Next.js serves and links it the same way. Use one manifest route, not both.
- Do not add the tags yourself. Leave
<link rel="icon">out ofapp/layout.tsx. In the App Router you never write a<head>element for this; Next.js composes the head from file conventions and your exportedmetadata. - Build and inspect the output. Run your normal build (or dev server), open any page, and view source: you should find the generated
rel="icon",rel="apple-touch-icon"andrel="manifest"tags. Open the emittedhrefvalues directly to confirm each one returns the new image rather than a 404 page.
When file conventions are not enough: metadata.icons
The conventions cover the standard set, but they cannot express everything: several raster sizes, per-scheme icons chosen with a media attribute, a shortcut icon, or files that must stay under public/ (for example behind a CDN path). For those, declare the icons through the Metadata API in your root layout and put the referenced files in public/:
// app/layout.tsx
import type { Metadata } from 'next';
export const metadata: Metadata = {
icons: {
icon: [
{ url: '/icon.svg', type: 'image/svg+xml' },
{ url: '/favicon-96x96.png', sizes: '96x96', type: 'image/png' },
],
shortcut: '/favicon.ico',
apple: { url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
},
manifest: '/site.webmanifest',
};
Treat this as an alternative, not an addition. If an app/icon.svg convention file and a metadata.icons entry describe the same icon, the page can end up with duplicate or competing tags and the browser will quietly use one of them. Pick one mechanism per icon: conventions for the default package above, or metadata when you need the extra control — and if you move an icon to metadata, remove its convention file.
One dark-mode note: a single SVG whose internal @media (prefers-color-scheme: dark) rule swaps its colours works through both routes and is usually simpler than two scheme-specific files. Our guide to SVG favicons and dark mode shows that setup, including why the raster fallbacks still need to be theme-neutral.
Still on the Pages Router?
None of the above applies under pages/: the app/ file conventions and the Metadata API are App Router features. A Pages Router site keeps the classic approach — files in public/ and explicit <link> tags via next/head or a custom pages/_document. Do not half-migrate by dropping an icon.svg into a pages/ project and waiting for it to be picked up; it will not be. The guide to which favicon files you need explains what each of those classic tags is for.
Troubleshooting: the icon is there but does not show
- The files are in
public/.public/favicon.icois served at/favicon.ico, so tabs may still find it by convention — butpublic/icon.svggets no generated tag and is effectively invisible. Move convention files intoapp/. - The names are the plain-HTML ones. Inside
app/it must beicon.svg,icon.pngandapple-icon.png— notfavicon.svgorapple-touch-icon.png(those names belong to thepublic/+metadataroute above). favicon.icois in a subfolder. Unlike the broader metadata conventions,favicon.icois only picked up at the top level ofapp/.- You declared it twice. A leftover
metadata.iconsblock, manual tags, or a previouspublic/set can shadow the convention files. Keep one set and remove the others. - It is just the cache. Browsers keep favicons in a stubborn, separate cache. Test in a private window, open the icon URL from the page source and hard-refresh it, and remember that Google refreshes search-result favicons on its own schedule over days. More causes, in order, are in why your favicon isn’t showing up.
- One platform still looks wrong. Safari 18 and earlier lean on the PNG/ICO because they do not support SVG favicons (Safari 26 and later do), iOS uses only the 180×180 Apple icon, and Android uses the manifest icons and crops them to the launcher shape. Debug the file that platform actually reads — our sizes and formats cheat sheet maps each platform to its file.
Verify the deployed site
When the build is live, run the URL through the favicon checker: it reads the generated tags from your real pages, downloads every icon and the manifest, validates formats and sizes, and previews the result in browser tabs, Google results, iOS and Android. A clean check plus a private-window test means any remaining old icon is local caching, not your Next.js setup.
Done properly, a Next.js favicon is a five-minute job with nothing to maintain: the right file names in app/, two manifest PNGs in public/, no hand-written tags, and metadata.icons held in reserve for the day you need sizes or variants the conventions cannot say. Generate the package, drop it in, and let the framework do the declaring.