Favicon Setup for Next.js — Free, Fast & Private
Next.js is the only framework in this list that will generate the <head> markup for you, and that is exactly where people get tripped up: pick one mechanism and stay in it. If you drop app/icon.png in AND declare icons in the metadata export, you get two competing <link rel="icon"> tags in the rendered head and a browser picking one at random.
Where each generated file goes in Next.js
The generator above emits seven files with fixed names. Static assets in Next.js live in app/ (file conventions) or public/, and the markup is written in app/layout.js — metadata export. This is the mapping between the two.
| Generated file | Goes to | Notes |
|---|---|---|
| favicon.ico | app/favicon.ico | Root app segment only. Next emits the link tag for you. |
| favicon-32x32.png | app/icon.png | Rename it. Next reads the real pixel size and writes sizes= itself. |
| apple-touch-icon.png | app/apple-icon.png | Rename it. Becomes rel="apple-touch-icon" automatically. |
| favicon-16x16.png | app/icon0.png / icon1.png | Numbered suffixes when you want more than one <link rel="icon">. |
| favicon-48x48.png | public/favicon-48x48.png | Already inside favicon.ico. Keep loose only if you link it by hand. |
| android-chrome-192x192.png | public/android-chrome-192x192.png | Never linked from HTML — referenced from the manifest. |
| android-chrome-512x512.png | public/android-chrome-512x512.png | Manifest icon for PWA install and Android splash. |
Route one: the app/ file conventions
Since the App Router landed, Next.js treats certain filenames inside app/ as route handlers that emit metadata. Drop a file named icon.png, icon.svg, icon.jpg or icon.ico into a segment folder and Next serves it and injects the matching <link rel="icon"> into every page rendered under that segment. Do the same with apple-icon.png and you get rel="apple-touch-icon". You write no markup at all.
favicon.ico is the exception with a real constraint attached: it is only honoured in the root app/ directory, not in nested segments. Put app/(marketing)/favicon.ico somewhere deeper and it is silently ignored. The icon and apple-icon conventions, by contrast, work at any depth, which is how a docs section or a whitelabelled dashboard route can carry its own icon while the rest of the site keeps the default.
The best part of this route is free cache busting. Next appends a content hash to the emitted URL — /icon.png?f8a2c1 — so when you replace the file the query string changes and browsers fetch the new icon instead of serving the sticky cached one for a week. That single behaviour solves the most common favicon support ticket there is.
File layout — the convention route
app/ favicon.ico ← root segment only; 16+32+48 inside icon.png ← rename favicon-32x32.png apple-icon.png ← rename apple-touch-icon.png layout.js public/ android-chrome-192x192.png android-chrome-512x512.png site.webmanifest
Route two: public/ plus the metadata export
The convention route hides the markup, which is fine until you need control over it — a sizes attribute Next did not guess the way you wanted, an icon served from a CDN, or a set you want to keep byte-identical to what the rest of your infrastructure expects. In that case put all seven generated files in public/ under their original names and declare them explicitly from the metadata export in app/layout.js.
Anything in public/ is served from the URL root verbatim: public/favicon.ico is /favicon.ico. Next does not fingerprint or transform these files, which is the flip side of the convention route — no automatic hash means you own the cache-busting problem. If you ship a redesign, either version the filename or accept that returning visitors keep the old icon until their cache expires.
One deployment detail that catches teams shipping under a path prefix: if basePath is set in next.config.js, public/ assets are served under that prefix, but strings you write into the metadata export are not rewritten. Either include the prefix in the URLs yourself or read it from a shared constant. This is the failure mode where the favicon works locally and 404s in staging.
app/layout.js — explicit declaration
export const metadata = {
icons: {
icon: [
{ url: '/favicon.ico', sizes: '16x16 32x32 48x48' },
{ url: '/favicon-32x32.png', type: 'image/png', sizes: '32x32' },
{ url: '/favicon-16x16.png', type: 'image/png', sizes: '16x16' },
],
apple: [{ url: '/apple-touch-icon.png', sizes: '180x180' }],
},
manifest: '/site.webmanifest',
};The manifest, and why two PNGs never appear in your HTML
android-chrome-192x192.png and android-chrome-512x512.png are not <link> targets. They belong in the web app manifest, which is what Chrome on Android reads when a user taps "Add to Home Screen" and what supplies the splash screen image on launch. You can ship a static public/site.webmanifest and point at it from metadata.manifest, or use the app/manifest.js file convention and return the object from code — useful when the app name or theme colour comes from an environment variable.
Getting this wrong is invisible in a desktop browser and obvious the moment someone installs the app: a generic Chrome icon on the home screen, or a blank white splash. If you support installs at all, test it on a real Android device rather than trusting the tab icon.
app/manifest.js — generated at build time
export default function manifest() {
return {
name: 'Your App',
short_name: 'App',
start_url: '/',
display: 'standalone',
icons: [
{ src: '/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: '/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' },
],
};
}Pages Router, and static export
None of the app/ conventions exist in the Pages Router. There, public/ is the only home for the files and the tags go inside a next/head block — in pages/_document.js if you want them on every page, which is almost always what you want for favicons. Putting them in _app.js works too but re-runs the head reconciler on every client navigation for no benefit. Projects part-way through an App Router migration should check both trees; a leftover _document.js still renders for any route the Pages Router still owns.
With output: "export" the story is simpler than people expect. public/ is copied into out/ untouched, and the icon route handlers are pre-rendered into static files with their hashed URLs baked into the HTML. A statically exported Next site needs no special favicon handling — just confirm your host is not stripping the query string, because some naive static servers treat /icon.png?f8a2c1 as a missing file.
Whatever you paste, the artwork itself never leaves your machine. The set above is rendered with the Canvas API and the .ico container is assembled in JavaScript on your device, so an unreleased logo stays unreleased. If the 16px preview looks like mush, that is the honest signal to simplify the mark before it ships to a Next.js tab near you.
