How to Add a Favicon to a Next.js App (App Router)
The exact files and folder structure Next.js's App Router expects for a favicon, step by step.
· 5 min read
Quick answer: drop a favicon.ico straight into your app/ folder and Next.js will pick it up automatically — no <link> tag needed. For more sizes and touch icons, add icon.png and apple-icon.png files anywhere in app/, and Next generates the correct <head> tags for you at build time.
Next.js's App Router treats favicons as a file convention, not a manual HTML edit. Instead of hand-writing <link rel="icon"> tags like you would on a static HTML site, you name a file correctly, put it in the right folder, and the framework injects the metadata for you. It's a different mental model, and it trips people up if they're used to the old way.
This guide covers exactly where each file goes, what happens with multiple sizes, and the code-based alternative for icons generated at build or request time.
The one exception: favicon.ico
favicon.ico is the only icon file Next.js treats as special in an unusual way: it has to sit directly inside the root app/ folder, not in a subfolder, and it must be a .ico file specifically — no .png substitute for this exact filename.
app/
favicon.ico
layout.tsx
page.tsx
That's it. No import, no config. Next detects it during the build and serves it at /favicon.ico, which matters because browsers request that exact path directly regardless of what's in your <head> — see our favicon size guide for why that legacy fallback still matters in 2026.
Everything else: icon and apple-icon
For every other icon, Next.js looks for specially named files anywhere inside app/ (not just the root) using these conventions:
| Filename pattern | Purpose |
|---|---|
icon.(ico|jpg|jpeg|png|svg) |
Standard favicon, rendered as <link rel="icon"> |
apple-icon.(jpg|jpeg|png) |
Apple touch icon, rendered as <link rel="apple-touch-icon"> |
Unlike favicon.ico, these accept several image formats, and Next will read the file's actual dimensions to populate the sizes attribute automatically. Drop a 180×180 apple-icon.png in app/ and you get a correctly sized <link rel="apple-touch-icon" sizes="180x180"> with zero manual markup.
You can also nest these inside route segments. An icon.png placed in app/blog/icon.png overrides the root icon for every page under /blog, which is handy for a multi-section site where sub-areas want a distinct tab icon.
Multiple sizes with numbered suffixes
If you want to ship more than one size of the same icon type, append a number before the extension:
app/
icon1.png
icon2.png
apple-icon1.png
apple-icon2.png
Next generates a separate <link> tag for each one, each with its own sizes value read from the file. This is the App Router's equivalent of manually listing a 16×16 and a 32×32 PNG side by side in a plain HTML <head>.
Generating icons in code with ImageResponse
Static image files cover most cases, but sometimes you want an icon built dynamically — pulling a color from a theme, rendering initials, or generating a variant per environment. For that, Next.js lets you export an icon.tsx (or apple-icon.tsx) file that returns an image instead of shipping one.
// app/icon.tsx
import { ImageResponse } from 'next/og'
export const size = { width: 32, height: 32 }
export const contentType = 'image/png'
export default function Icon() {
return new ImageResponse(
(
<div
style={{
fontSize: 24,
background: '#111',
color: '#fff',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
F
</div>
),
{ ...size }
)
}
Next runs this at build time for static rendering, or on demand if the route is dynamic, and serves the result at the same /icon path a static file would use. apple-icon.tsx works identically, just for the touch icon slot. This is the exception rather than the rule — for a one-off logo or wordmark, running it through the favicon generator once and dropping in a static PNG is simpler and doesn't cost a render on every request.
The web app manifest: app/manifest.ts
If you want your app installable as a PWA with a proper home-screen icon on Android, Next.js also supports a code-based manifest at app/manifest.ts:
// app/manifest.ts
import type { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'My App',
short_name: 'App',
icons: [
{ src: '/icon-192.png', sizes: '192x192', type: 'image/png' },
{ src: '/icon-512.png', sizes: '512x512', type: 'image/png' },
],
start_url: '/',
display: 'standalone',
}
}
Next compiles this into /manifest.webmanifest and links it automatically, the same way it handles icon.png. If you're not familiar with what each manifest icon size is actually for, our guide to web app manifest icons covers it in more depth than fits here.
You don't write the <link> tags yourself
This is the part that's easy to miss coming from a plain HTML background: with the App Router, you never touch <head> directly for any of this. Next's metadata system reads whichever icon files exist and injects the matching <link> tags into every page automatically. If you've previously hand-coded favicon markup by following a full HTML tag reference, resist the urge to also paste that snippet into a layout file — Next is already generating equivalent tags, and duplicating them just creates redundant or conflicting entries in <head>.
Why your favicon might not update in dev
Favicons are one of the most aggressively cached assets in a browser, often surviving a hard refresh that clears everything else. If you swap app/icon.png for a new image and the browser tab still shows the old one, that's almost always the browser, not your code. Try opening the tab in a private window, or hitting the icon URL directly in a fresh tab, before assuming your file conventions are wrong. If it's still stale after that, run the site through a favicon checker to confirm what's actually being served versus what your browser has cached — and if you're chasing this specific problem in production, our favicon cache guide walks through forcing a refresh properly.
✦ Try the Generator
Drop in artwork, type a word, or pick an emoji. Watch it land in a real browser tab, then export every size a modern site needs.
Open Generator