Platform Guides

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.

7 min read
Free Guide

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
  1. 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:

  1. 1Build locally: npm run build && npm run start
  2. 2Check generated HTML: View source for meta tags
  3. 3Test all routes: Ensure route-specific icons work
  4. 4Verify dark mode: Toggle system theme
  5. 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

  1. 1Use PNG over ICO: Better quality, smaller files
  2. 2Implement apple-icon: Critical for iOS users
  3. 3Consider SVG: Scalable and tiny
  4. 4Test dark mode: Growing number of users
  5. 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.