Next.js 15 Favicon Guide: App Router Best Practices
Modern favicon implementation for Next.js 15 using the App Router. File-based metadata API and advanced configurations.
Next.js 15's Elegant Favicon Solution
Next.js 15 with the App Router has revolutionised how we handle metadata, including favicons. Gone are the days of wrestling with webpack configs or manually adding link tags. The new file-based metadata API is so intuitive, it feels like cheating. Drop a file in the right place, and Next.js handles everything else - generating multiple sizes, adding proper tags, even supporting different icons for light and dark modes.
This guide covers the modern approach using Next.js 15's App Router. If you're still on the Pages Router, the concepts differ significantly - this is all about embracing the new paradigm.
The File-Based Approach (Recommended)
Simple Favicon Setup
The easiest method requires just one step:
app/
└── favicon.ico
- 1Add your favicon to your app directory:
That's it. Next.js automatically:
- Serves it at
/favicon.ico
- Adds the appropriate
<link>
tag - Handles caching headers
- Optimises delivery
Modern Icon Setup
For better quality and device support, use the icon file convention:
app/
├── icon.png # 32x32 favicon
├── icon.svg # Scalable version (optional)
└── apple-icon.png # 180x180 Apple touch icon
Next.js generates all necessary tags:
<link rel="icon" href="/icon?<generated>" type="image/png" sizes="32x32" />
<link rel="apple-touch-icon" href="/apple-icon?<generated>" type="image/png" sizes="180x180" />
Advanced Icon Configuration
Multiple Sizes
Need specific sizes? Use the file naming convention:
app/
├── icon1.png # 32x32 (default)
├── icon2.png # 16x16
└── icon3.png # Any custom size
Or better yet, use the metadata file approach for full control.
Dynamic Icon Generation
Create an icon.tsx
file for programmatic generation:
// app/icon.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const size = {
width: 32,
height: 32,
}
export const contentType = 'image/png'
export default function Icon() {
return new ImageResponse(
(
<div
style={{
fontSize: 24,
background: '#000',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
borderRadius: 8,
}}
>
A
</div>
),
{
...size,
}
)
}
This generates icons dynamically - perfect for:
- User-specific icons
- A/B testing
- Seasonal variations
- Environment indicators
Apple Touch Icon
For iOS devices, create apple-icon.tsx
:
// app/apple-icon.tsx
import { ImageResponse } from 'next/og'
export const runtime = 'edge'
export const size = {
width: 180,
height: 180,
}
export const contentType = 'image/png'
export default function AppleIcon() {
return new ImageResponse(
(
<div
style={{
fontSize: 140,
background: 'linear-gradient(to bottom right, #000, #333)',
width: '100%',
height: '100%',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: '#fff',
borderRadius: 36,
}}
>
A
</div>
),
{
...size,
}
)
}
Metadata API Approach
For fine-grained control, use the metadata API:
// app/layout.tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
icons: {
icon: [
{ url: '/icon-16x16.png', sizes: '16x16', type: 'image/png' },
{ url: '/icon-32x32.png', sizes: '32x32', type: 'image/png' },
],
shortcut: '/favicon.ico',
apple: [
{ url: '/apple-icon.png' },
{ url: '/apple-icon-180x180.png', sizes: '180x180', type: 'image/png' },
],
other: [
{
rel: 'mask-icon',
url: '/safari-pinned-tab.svg',
color: '#000000',
},
],
},
manifest: '/site.webmanifest',
}
Route-Specific Favicons
Different favicons for different sections? Easy:
// app/admin/layout.tsx
export const metadata: Metadata = {
icons: {
icon: '/admin-icon.png',
},
}
// app/shop/layout.tsx
export const metadata: Metadata = {
icons: {
icon: '/shop-icon.png',
},
}
This creates distinct visual indicators for different app sections - particularly useful for admin areas or multi-tenant applications.
Dark Mode Support
Next.js 15 supports adaptive icons natively:
// app/layout.tsx
export const metadata: Metadata = {
icons: {
icon: [
{ url: '/icon-light.png', media: '(prefers-color-scheme: light)' },
{ url: '/icon-dark.png', media: '(prefers-color-scheme: dark)' },
],
},
}
Or use the file convention:
app/
├── icon.png # Light mode
└── icon-dark.png # Dark mode (automatically detected)
Static vs Dynamic Generation
Static Icons (Default)
Place files in the app directory for static serving:
app/
├── favicon.ico
├── icon.png
└── apple-icon.png
Benefits:
- Zero runtime overhead
- CDN-friendly
- Predictable URLs
- Best performance
Dynamic Icons
Use .tsx
files for runtime generation:
// app/icon.tsx
export default function Icon({ params }: { params: { slug: string } }) {
// Generate based on params, headers, cookies, etc.
}
Use cases:
- User avatars as favicons
- Environment indicators (dev/staging/prod)
- A/B testing different icons
- Personalisation
Web App Manifest
For PWA support, add a static manifest:
// app/manifest.json
{
"name": "My Next.js App",
"short_name": "NextApp",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#000000",
"background_color": "#ffffff",
"display": "standalone"
}
Or generate dynamically:
// app/manifest.ts
import { MetadataRoute } from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'My Next.js App',
short_name: 'NextApp',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png',
},
],
theme_color: '#000000',
background_color: '#ffffff',
display: 'standalone',
}
}
Performance Optimisation
Image Optimisation
Next.js automatically optimises static icons:
- Compression
- Format selection
- Cache headers
- CDN-ready URLs
For dynamic icons, optimise manually:
export default function Icon() {
return new ImageResponse(
// Your JSX here
{
width: 32,
height: 32,
// Reduce quality for smaller files
quality: 85,
// Enable edge runtime for global performance
runtime: 'edge',
}
)
}
Preloading
For critical icons, add preload hints:
export const metadata: Metadata = {
other: {
'link rel="preload"': '/icon.png as="image"',
},
}
Common Patterns
Environment-Specific Icons
Show different icons per environment:
// app/icon.tsx
export default function Icon() {
const isDev = process.env.NODE_ENV === 'development'
return new ImageResponse(
(
<div style={{
background: isDev ? '#f00' : '#000',
// ... rest of styles
}}>
{isDev ? 'D' : 'P'}
</div>
)
)
}
Animated Favicons
While not recommended, it's possible:
// app/icon.gif/route.ts
export async function GET() {
// Generate or serve animated GIF
// Note: Limited browser support
}
Migration from Pages Router
Moving from Pages Router? Key differences:
Pages Router (old):
// pages/_document.js
<link rel="icon" href="/favicon.ico" />
App Router (new):
// Just add files:
app/favicon.ico
app/icon.png
No more manual tags, no more document modifications.
Testing Your Favicons
Verify your implementation:
- 1Build locally:
npm run build && npm run start
- 2Check generated HTML: View source for meta tags
- 3Test all routes: Ensure route-specific icons work
- 4Verify dark mode: Toggle system theme
- 5Mobile testing: Add to home screen on devices
Creating Icons for Next.js
Need perfectly sized icons? Try Unwrite's Favicon Generator. Upload your logo and download all required sizes for Next.js, including apple-touch-icon and multiple PNG sizes. Everything processes privately in your browser.
Best Practices
- 1Use PNG over ICO: Better quality, smaller files
- 2Implement apple-icon: Critical for iOS users
- 3Consider SVG: Scalable and tiny
- 4Test dark mode: Growing number of users
- 5Keep it simple: Complex icons don't scale
The Path Forward
Next.js 15's approach to favicons exemplifies the framework's philosophy: convention over configuration, with escape hatches when needed. Start with the simple file-based approach, then add complexity only if required.
The beauty lies in the simplicity - drop a file, get a favicon. Need more control? The APIs are there. It's modern web development at its finest.