你在 app 目录下放了 favicon.ico,能用。但如果营销页和后台需要不同的图标,或者图标要根据用户主题偏好切换且不能整页刷新呢?文件约定很方便,但覆盖不了所有场景。

Next.js 在 metadata 对象里暴露了 icons 字段,支持以编程方式声明图标。这样你可以精细控制 rel、sizes、type,甚至 media 属性——全部在 layout.tsx 或某个 page.tsx 里完成。下面一步步演示如何从静态文件迁移到 metadata 驱动的方式。

前置条件

第一步:定义可复用的图标配置

写一个小 helper,让你能在不同 layout 里复用同一套图标定义。这样 metadata 导出更干净,后续更新也省事。

// lib/icons.ts
import { Metadata } from 'next';

export const iconConfig = {
  icon: [
    { url: '/icons/icon-16x16.png', sizes: '16x16', type: 'image/png' },
    { url: '/icons/icon-32x32.png', sizes: '32x32', type: 'image/png' },
    { url: '/icons/icon.svg', sizes: 'any', type: 'image/svg+xml' },
  ],
  apple: [
    { url: '/icons/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
  ],
  shortcut: '/icons/favicon.ico',
  other: [
    {
      rel: 'mask-icon',
      url: '/icons/safari-pinned-tab.svg',
      color: '#0066cc',
    },
  ],
} satisfies Metadata['icons'];

第二步:在根 layout 中应用配置

导入 helper 并展开到 metadata 导出的 icons 字段里。这样就不用在 app/ 下直接放 favicon.ico、icon.png 和 apple-icon.png 了。

// app/layout.tsx
import type { Metadata } from 'next';
import { iconConfig } from '@/lib/icons';

export const metadata: Metadata = {
  title: 'My App',
  icons: iconConfig,
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body>{children}</body>
    </html>
  );
}

第三步:为特定路由覆盖图标

因为 metadata 会沿路由树向下合并,页面可以扩展或替换根 icon 集合。适合做分区 branding——比如博客子目录或租户后台用不同的图标。

// app/dashboard/layout.tsx
import type { Metadata } from 'next';
import { iconConfig } from '@/lib/icons';

export const metadata: Metadata = {
  icons: {
    ...iconConfig,
    icon: [
      { url: '/icons/dashboard-32x32.png', sizes: '32x32', type: 'image/png' },
      { url: '/icons/dashboard.svg', sizes: 'any', type: 'image/svg+xml' },
    ],
  },
};

export default function DashboardLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return <section>{children}</section>;
}

第四步:根据数据动态生成图标

需要根据 feature flag、用户角色或 A/B 变体切换图标?在 server component 里取数据,动态构建 icons 对象。下面例子在 theme cookie 为 dark mode 时选用高对比度变体。

// app/settings/page.tsx
import { cookies } from 'next/headers';
import type { Metadata } from 'next';

export async function generateMetadata(): Promise<Metadata> {
  const cookieStore = await cookies();
  const theme = cookieStore.get('theme')?.value ?? 'light';
  const suffix = theme === 'dark' ? '-dark' : '';

  return {
    icons: {
      icon: [
        { url: `/icons/settings${suffix}-32x32.png`, sizes: '32x32' },
        { url: `/icons/settings${suffix}.svg`, sizes: 'any' },
      ],
      apple: [{ url: `/icons/settings${suffix}-apple.png`, sizes: '180x180' }],
    },
  };
}

export default function SettingsPage() {
  return &lt;main&gt;Settings&lt;/main&gt;;
}

第五步:用 media query 添加 dark-mode SVG

如果偏好单文件 SVG 通过 CSS 自适应,声明一次,让 media 属性处理剩余逻辑。避免重复资源,保持 &lt;head&gt; 精简。

// lib/icons.ts(补充)
export const adaptiveIconConfig = {
  icon: [
    {
      url: '/icons/adaptive.svg',
      sizes: 'any',
      type: 'image/svg+xml',
      media: '(prefers-color-scheme: light)',
    },
    {
      url: '/icons/adaptive-dark.svg',
      sizes: 'any',
      type: 'image/svg+xml',
      media: '(prefers-color-scheme: dark)',
    },
  ],
} satisfies Metadata['icons'];

常见坑

验证清单

  1. 打开 DevTools → Network → 筛选 "img",确认每个声明的图标都返回 200。
  2. 检查 &lt;head&gt; 里的 &lt;link rel='icon'&gt;、&lt;link rel='apple-touch-icon'&gt; 和 &lt;link rel='mask-icon'&gt; 标签。
  3. 在 OS 设置里切换 dark mode;正确的 SVG 应在不刷新的情况下加载。
  4. 用 Mzu favicondl 的在线检查工具 快速审计,找出缺失的尺寸或 MIME 类型不匹配。

什么时候继续用文件约定

如果你的项目只需要一套静态 favicon,文件式方案(app/favicon.ico、app/icon.png 等)仍然是最简单的路径。它不需要 TypeScript、没有运行时开销,开箱即用。当你需要按路由覆盖、动态选择,或者 media、color 这类高级属性时,再用 metadata API。

想深入了解文件式工作流,可以看我们这篇 几分钟搞定 Next.js App Router favicon。如果你在做 PWA,Web App Manifest favicon 指南 还涵盖了这些 metadata 声明之外需要的 manifest 图标。

查看官方文档要求

Next.js 在 metadata 文件约定 里记录了 favicon 和 app icon 的规范。