Next.js favicon not working: app/favicon.ico vs public/
Reproduced on Next.js 16.4.2: with an icon in both app/ and public/, next dev and next start served different files without a warning. Where each icon file must live, and what renaming to icon.ico really changes.
TL;DR#
In an App Router project, create-next-app puts a favicon.ico inside app/, not public/. That file is a metadata file: Next.js adds its own <link rel="icon"> tag for it and serves it at /favicon.ico. If you then drop your own icon into public/favicon.ico, two files answer the same URL. On Next.js 16.4.2, next dev served the template icon and next start served the one from public/. The build succeeded and there was no warning. Fix: keep exactly one icon. Overwrite app/favicon.ico with yours (or src/app/favicon.ico if you use a src folder), or delete it, keep public/favicon.ico and declare it with the icons field of the root layout's metadata. The popular "rename it to icon.ico" answer gives your icon a URL nothing else claims, but it leaves the template icon linked as well if app/favicon.ico is still there.
The question, and why the answers disagree#
The Stack Overflow question Why is my favicon not working in my next js app? dates from May 2020 and describes a Pages Router setup:
dir/pages/index.js
dir/public/favicon.icowith this in the page:
<Head>
<title>Create Next App</title>
<link rel="icon" href="/favicon.ico" />
</Head>The highest-voted answer, written years later, says "move your icon to app directory and just rename the favicon.ico to icon.ico". That answers a different project. The app/ directory did not exist for the asker, and the icon file conventions it relies on were added in Next.js 13.3.0, according to the version table of the favicon, icon and apple-icon reference. Readers with an App Router project are more likely to hit the cause reproduced below. The Pages Router case is covered further down.
What the duplicate icon looks like#
The test below runs Next.js 16.4.2 with Turbopack and React 19.3.1, on Node.js 24.21.0 in the official node:24-alpine Docker image (22 October 2026). It uses two 16×16 ICO files of different colours, so the MD5 of each response shows which file the server returned. The red file stands in for the template's app/favicon.ico and the blue file is "your" icon in public/:
8edf86cf8c55df30ef66cd50713113cd app/favicon.ico
f358bb1107ff9e34e85ed6ef7fd4e094 public/favicon.icoUnder next dev, the page links the app/ icon and /favicon.ico returns the red file:
### S1: app/favicon.ico (red) + public/favicon.ico (blue) [next dev]
<link rel="icon" href="/favicon.ico?favicon.3f5hpse212nol.ico" sizes="16x16" type="image/x-icon"/>
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=no-cache, must-revalidate md5=8edf86cf8c55df30ef66cd50713113cd (red icon file)After next build (exit code 0) and next start, the same tag is rendered, but the same URL now returns the blue file from public/:
### S1: app/favicon.ico (red) + public/favicon.ico (blue) [next prod]
next build exit code: 0
<link rel="icon" href="/favicon.ico?favicon.3f5hpse212nol.ico" sizes="16x16" type="image/x-icon"/>
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0 md5=f358bb1107ff9e34e85ed6ef7fd4e094 (blue icon file)Neither server printed a warning or an error. So the same project served the default icon under next dev and yours under next start, from the same source tree. This test did not deploy to a hosting platform, so it says nothing about which file a given host serves; the safe reading is that the result is undefined until only one file is left.
Why it happens#
The App Router treats a few file names in app/ as metadata, not as static assets. The reference page states it directly: "Use an image file to set an app icon by placing a favicon, icon, or apple-icon image file within your /app directory. The favicon image can only be located in the top level of app/." For such a file, "Next.js will evaluate the file and automatically add the appropriate tags to your app's <head> element."
The App Router template in the Next.js repository ships app/favicon.ico, and its public/ folder holds only SVG files. The Pages Router template (templates/default/js/public) keeps favicon.ico in public/. Habits and older tutorials point to public/, and the App Router public folder reference now says otherwise: "For static metadata files, such as robots.txt, favicon.ico, etc, you should use special metadata files inside the app folder."
The Pages Router refuses this kind of overlap. Its conflicting public file message says "Since only one resource can reside at the URL both public files and page files must be unique". A metadata favicon.ico in app/ next to public/favicon.ico did not trigger any such check in the test. Neither the icon reference nor the public folder reference says which file wins when both exist, and the test shows that the answer depends on the server you run.
Fix: keep one favicon#
- Find every icon file Next.js may pick up:
find app src/app public -iname '*icon*' 2>/dev/null- Choose the App Router convention and overwrite the template file with your own icon. Use
src/app/favicon.icoinstead if your project uses asrcfolder:
cp ~/Downloads/favicon.ico app/favicon.ico- Delete the duplicate in
public/so only one file answers/favicon.ico:
rm public/favicon.ico- If you prefer to keep the icon in
public/, delete the template file instead. Next.js renders no icon tag for a file inpublic/(scenario S3 below), so declare it in the root layout with theiconsfield of the metadata object, which renders a<link rel="icon">tag (scenario S3b below). The same page recommends the file-based convention "where possible", because it keeps the tag and the file in sync:
rm app/favicon.ico// app/layout.js
export const metadata = {
icons: { icon: '/favicon.ico' },
}- Need a PNG or SVG icon, or an Apple touch icon, as well? Add
icon.pngoricon.svgandapple-icon.pngnext tolayout.jsinapp/. The reference lists.ico,.jpg,.jpeg,.pngand.svgforicon,.jpg,.jpegand.pngforapple-icon, and.icoonly forfavicon.
Verify the fix#
Restart the server, then check what the HTML links and which bytes the URL returns:
curl -s http://localhost:3000/ | grep -o '<link rel="icon"[^>]*>'
curl -sI http://localhost:3000/favicon.ico
md5sum app/favicon.ico # or: md5sum public/favicon.ico if you kept the icon in public/
curl -s http://localhost:3000/favicon.ico | md5sumThe two hashes must match; compare against the file you kept, in app/ or in public/. With only app/favicon.ico left and its content replaced, the production build linked a new URL and served the new file:
### S2b: only app/favicon.ico, content now blue [next prod]
<link rel="icon" href="/favicon.ico?favicon.22286bb5egam5.ico" sizes="16x16" type="image/x-icon"/>
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0, must-revalidate md5=f358bb1107ff9e34e85ed6ef7fd4e094 (blue icon file)The query string after /favicon.ico changed from favicon.3f5hpse212nol.ico (red content) to favicon.22286bb5egam5.ico when only the file's content changed. With the icon in public/ and nothing in app/ (and no icons field yet), no link tag is rendered and the file is still served at the root:
### S3: only public/favicon.ico (blue), no favicon in app/ [next prod]
(none)
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0 md5=f358bb1107ff9e34e85ed6ef7fd4e094 (blue icon file)Adding the icons field from step 4 to the root layout renders the tag for the file in public/:
export const metadata = { title: 'favicon repro', icons: { icon: '/favicon.ico' } }
### S3b: only public/favicon.ico (blue) + metadata.icons in layout [next prod]
<link rel="icon" href="/favicon.ico"/>
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0 md5=f358bb1107ff9e34e85ed6ef7fd4e094 (blue icon file)Unlike the metadata file, this tag carries no query string, so it does not change when the file's content changes.
The server checks above prove what Next.js sends. What the tab finally shows also depends on things this test does not cover: the browser's own favicon cache, and the ICO file itself. When the right bytes come back but the tab stays unchanged, suspect the cache or the ICO itself: the Pages Router section below quotes a report of an icon that only displayed after being reconverted.
One email a month — the latest explained errors
What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.
What renaming to icon.ico really changes#
Renaming gives your icon its own URL, and what happens next depends on what is left beside it. With app/icon.ico (red) and the old public/favicon.ico (blue) still in place, the page links /icon.ico, a URL nothing else claims, while /favicon.ico keeps serving the file from public/:
### S4: app/icon.ico (red) + public/favicon.ico (blue) [next prod]
<link rel="icon" href="/icon.ico?icon.3f5hpse212nol.ico" sizes="16x16" type="image/x-icon"/>
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0 md5=f358bb1107ff9e34e85ed6ef7fd4e094 (blue icon file)
GET /icon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0, must-revalidate md5=8edf86cf8c55df30ef66cd50713113cd (red icon file)So the rename sidesteps the clash without removing it: anything that still requests /favicon.ico directly gets the other file.
Moving a favicon.ico into app/ overwrites the template file, so following the answer in that order leaves only icon.ico. If you rename your file to icon.ico before moving it into app/, the template app/favicon.ico stays, both are metadata files, and the page links both of them:
### S7: app/favicon.ico (red) + app/icon.ico (blue) [next prod]
<link rel="icon" href="/favicon.ico?favicon.3f5hpse212nol.ico" sizes="16x16" type="image/x-icon"/>
<link rel="icon" href="/icon.ico?icon.22286bb5egam5.ico" sizes="16x16" type="image/x-icon"/>
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0, must-revalidate md5=8edf86cf8c55df30ef66cd50713113cd (red icon file)
GET /icon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0, must-revalidate md5=f358bb1107ff9e34e85ed6ef7fd4e094 (blue icon file)Which of two equally sized icons a browser displays is the browser's choice, and this test does not cover it. The rename is not needed either: a single app/favicon.ico worked in the test, as shown in the previous section. The answer was written for "Nextjs latest version (v14)", but the convention it relies on dates from Next.js 13.3.0, so the same reasoning applies to any App Router project since then.
A favicon below the root segment is ignored#
The rule "only in the top level of app/" is strict. With app/blog/favicon.ico and no other icon, neither / nor /blog gets a link tag, and both URLs return 404:
### S5: favicon.ico below the root segment (app/blog/) [next prod]
(none)
GET /favicon.ico -> 404 content-type=text/html; charset=utf-8 cache-control=private, no-cache, no-store, max-age=0, must-revalidate
GET /blog/favicon.ico -> 404 content-type=text/html; charset=utf-8 cache-control=private, no-cache, no-store, max-age=0, must-revalidateThe build still exits with code 0. The reference says that "Favicons can only be set in the root /app segment. If you need more granularity, you can use icon." When migrating a site route by route, the Pages to App Router migration gotchas cover the other places where metadata ends up split between next/head and app/.
If you are on the Pages Router (the original question)#
There is no metadata file convention in pages/. The Pages Router public folder reference lists favicon.ico among the files public/ is meant for, and a file in public/ is served from the base URL, so public/favicon.ico with <link rel="icon" href="/favicon.ico" /> is the documented setup. The test above was run on the App Router only; for a Pages Router project, check the same two things with the curl commands from the previous section: the tag in the HTML and the bytes at /favicon.ico. If both are right, the remaining causes are outside Next.js. One answer on the question reports that the file was served correctly from http://localhost:3000/favicon.ico yet did not show in the tab until it was reconverted with a dedicated favicon converter, and suggests testing with a .png first.
When /favicon.ico redirects to the login page#
An auth proxy.js (the file convention formerly called middleware, renamed in Next.js 16.0.0 according to the proxy reference) without a matcher runs on every request, icon included. The test used a proxy that redirects every path except /login:
### S6: proxy.js without a matcher [next prod]
GET /favicon.ico -> 307 content-type=null location=/login cache-control=nullAdding a negative matcher that excludes the icon brings it back:
export const config = { matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'] }### S6b: proxy.js with a matcher excluding favicon.ico [next prod]
GET /favicon.ico -> 200 content-type=image/x-icon cache-control=public, max-age=0, must-revalidate md5=f358bb1107ff9e34e85ed6ef7fd4e094 (blue icon file)If your CSS, images or other files in public/ disappear in the same way, the cause is the same matcher: continue with proxy.js matcher not working for static assets, which also covers the auth gap the matcher leaves for Server Functions. If the proxy seems to run locally but not after deploying, see Next.js middleware not running on Vercel.
When the server returns the wrong icon, or none, the question is which file owns the URL /favicon.ico: the metadata file in app/, the static file in public/, or a proxy that redirects the request before either answers. Run the two curl checks above and keep one icon: then there is no second file left for next dev and next start to disagree about.
Frequently Asked Questions
Still stuck? Paste your error.
Paste the error message or stack trace — we check it against every fix and tool on the site.
Continue Reading
Fix: Next.js proxy.js matcher not working for static assets
Without a matcher, proxy.js runs on every request including _next/static and public assets. And Server Functions are not separate routes — a matcher gap silently drops auth coverage.
Next.js app/api 404 vs 405: route.js and named exports
Reproduced on Next.js 16.4.2: why app/api/admin/all.js answers 404, what a 405 means instead, and the static export case that only 404s in production.
Next.js Redirect from / to another page
If your redirect works only after render, flickers, or fires in the wrong place, you are probably using the wrong redirect API for the job. Here is the exact mapping.


