你在 app 目录下放了 favicon.ico,能用。但如果营销页和后台需要不同的图标,或者图标要根据用户主题偏好切换且不能整页刷新呢?文件约定很方便,但覆盖不了所有场景。
Next.js 在 metadata 对象里暴露了 icons 字段,支持以编程方式声明图标。这样你可以精细控制 rel、sizes、type,甚至 media 属性——全部在 layout.tsx 或某个 page.tsx 里完成。下面一步步演示如何从静态文件迁移到 metadata 驱动的方式。
前置条件
- 使用 App Router 的 Next.js 13+ 项目
- 放在
public/或可访问的 CDN 路径下的图标资源(SVG、PNG 或 ICO) - 熟悉
next中的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 <main>Settings</main>;
}
第五步:用 media query 添加 dark-mode SVG
如果偏好单文件 SVG 通过 CSS 自适应,声明一次,让 media 属性处理剩余逻辑。避免重复资源,保持 <head> 精简。
// 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'];
常见坑
- 忘了加
satisfies约束。没有它,TypeScript 不会捕获缺失的url或非法的rel值。 - 混用文件式和 metadata 图标。Next.js 会合并它们,但重复的
rel条目可能导致浏览器行为不可预测。每个路由段选一种策略。 - 相对 URL 没加前导斜杠。一定要以
/或绝对 CDN 地址开头;否则浏览器会按当前路径解析。 - 漏了 fallback ICO。部分企业代理和旧爬虫仍会请求
/favicon.ico。配置里保留shortcut: '/icons/favicon.ico'。
验证清单
- 打开 DevTools → Network → 筛选 "img",确认每个声明的图标都返回 200。
- 检查
<head>里的<link rel='icon'>、<link rel='apple-touch-icon'>和<link rel='mask-icon'>标签。 - 在 OS 设置里切换 dark mode;正确的 SVG 应在不刷新的情况下加载。
- 用 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 的规范。