Next.js Metadata、资源与 SEO
如果把 SEO 理解成“改个 title”,放在 Next 项目里通常会太浅。
更完整的理解应该是:
- 页面标题和描述
- Open Graph / Twitter Card
- canonical
- robots
- sitemap
- manifest
- 图标、OG 图、分享图
- 动态页面的 metadata 生成方式
到了 App Router,这一整套已经被 Next 收成了官方 Metadata API 和 metadata file conventions。
先看最核心的两条线
1. metadata / generateMetadata
用来定义页面或布局的 <head> 信息。
2. metadata files
用来定义一些特殊文件或资源,例如:
robots.tssitemap.tsmanifest.tsopengraph-image.tsxicon.tsx
这两条线一起组成了现代 Next 项目里最常见的 SEO 和分享能力。
metadata 适合静态信息
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: 'Docs',
description: 'Project documentation',
};
export default function Page() {
return <div>Docs</div>;
}
这适合:
- 固定标题
- 固定描述
- 固定分享信息
- 稳定页面段落
generateMetadata 适合动态信息
import type { Metadata } from 'next';
export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = await getPost(slug);
return {
title: post.title,
description: post.summary,
};
}
适合:
- 博客详情页
- 商品详情页
- 用户资料页
- 动态内容型页面
一个关键点是:generateMetadata 本身就是渲染的一部分。如果页面还能预渲染,而且 metadata 逻辑没有引入真正的动态行为,结果仍然可以进首屏 HTML。
这套 API 的边界
官方文档明确说明:
metadata和generateMetadata只支持 Server Components- 它们不是给 client component 用的
这很合理,因为 <head> 管理本来就更适合在服务端模型里完成。
最常见的 metadata 字段
标题与描述
export const metadata = {
title: 'Next.js Guide',
description: 'A detailed Next.js knowledge base',
};
metadataBase
export const metadata = {
metadataBase: new URL('https://example.com'),
};
这个字段很重要,因为很多相对路径 metadata 最终都要基于它展开。
Open Graph
export const metadata = {
openGraph: {
title: 'Next.js Guide',
description: 'A detailed Next.js knowledge base',
url: 'https://example.com/docs/next',
siteName: 'Example',
images: [
{
url: '/og/next.png',
width: 1200,
height: 630,
alt: 'Next.js Guide',
},
],
type: 'article',
},
};
Twitter Card
export const metadata = {
twitter: {
card: 'summary_large_image',
title: 'Next.js Guide',
description: 'A detailed Next.js knowledge base',
images: ['/og/next.png'],
},
};
canonical
export const metadata = {
alternates: {
canonical: 'https://example.com/docs/next',
},
};
这在多语言、分页、重复内容路径场景里尤其重要。
viewport 和旧字段的变化
较新的文档里,官方已经明确标注:
themeColor在metadata里已废弃colorScheme在metadata里也已废弃
现在更推荐走 viewport 配置。
robots.ts 怎么理解
robots.ts 是一类特殊 metadata file。
import type { MetadataRoute } from 'next';
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: ['/admin', '/private'],
},
sitemap: 'https://example.com/sitemap.xml',
};
}
它的价值主要是:
- 明确告诉搜索引擎哪些路径该抓、哪些别抓
- 把 sitemap 暴露给爬虫
常见误区是把它当成安全控制。实际上 robots.txt 只是爬虫协作协议,不是访问控制机制。
sitemap.ts 怎么理解
import type { MetadataRoute } from 'next';
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const posts = await getPosts();
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily',
priority: 1,
},
...posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: post.updatedAt,
changeFrequency: 'weekly' as const,
priority: 0.7,
})),
];
}
适合:
- 文档站
- 博客
- 商品站点
- 内容量较大的内容平台
较新的官方文档还提供了 generateSitemaps(),用来把超大站点拆成多份 sitemap。
manifest.ts
import type { MetadataRoute } from 'next';
export default function manifest(): MetadataRoute.Manifest {
return {
name: 'Example Docs',
short_name: 'Example',
description: 'Documentation site',
start_url: '/',
display: 'standalone',
background_color: '#ffffff',
theme_color: '#111111',
icons: [
{
src: '/icon-192.png',
sizes: '192x192',
type: 'image/png',
},
],
};
}
这和 SEO 不完全等价,但它常常和安装体验、PWA 外观、品牌感知放在一起整理,所以通常会归到这一组。
图标、OG 图、分享图
Next 还支持通过特殊文件约定处理:
favicon.icoicon.png/icon.tsxapple-icon.pngopengraph-image.png/opengraph-image.tsxtwitter-image.png/twitter-image.tsx
这类文件定义后,Next 会自动把相关 head 信息接起来。
动态页面里最容易踩的坑
1. canonical 和分享图没跟着动态路由变化
结果通常是:
- 所有详情页看起来 title 不同
- 但分享卡片或 canonical 还指向同一个固定地址
2. metadataBase 没设好
相对地址可能会在部署后生成错误链接。
3. sitemap 只放了静态页
内容型站点如果详情页没进 sitemap,抓取效率通常会差很多。
4. robots 当成权限控制
robots.txt 不能阻止真实访问,只能表达抓取建议。
一套比较稳的落地顺序
- 先在根布局补
metadataBase、全站 title template、description - 再给关键详情页补
generateMetadata - 再补
robots.ts和sitemap.ts - 再补 OG 图、Twitter 图、manifest 和图标
- 最后处理多语言 canonical、分页 canonical、分区爬虫策略
常见页面类型怎么配
文档站
重点:
- title template
- canonical
- sitemap
- robots
- OG 图
博客
重点:
- 文章级
generateMetadata - 文章更新时间进 sitemap
- article 类型的 openGraph
电商或商品站
重点:
- 商品详情 metadata 动态生成
- 分类与详情的 canonical
- 大规模 sitemap 拆分
图片、字体与第三方脚本
这块经常被拆成三个零散话题来看:
next/imagenext/fontnext/script
但放到一起看更容易理解。它们其实都在做同一类事:
- 优化首屏资源加载
- 减少布局抖动
- 控制第三方资源对性能的影响
- 让资源加载策略更明确
next/image 解决什么问题
官方的 <Image /> 不是普通 <img> 的简单封装,它主要解决:
- 自动尺寸优化
- 现代格式输出
- 原生懒加载
- 避免布局偏移
- 远程图片安全白名单控制
import Image from 'next/image';
export default function Avatar() {
return (
<Image
src="/profile.png"
alt="Profile"
width={500}
height={500}
/>
);
}
为什么它比 <img> 更值得默认用
最直接的原因有三个:
- 宽高信息明确,能避免 CLS
- 浏览器不会一次性把所有图片都抢先拉下来
- 可以根据设备和视口输出更合适的资源尺寸
本地图片和远程图片的区别
本地图片
放在项目内,Next 更容易推断资源。
远程图片
要显式允许来源。
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
port: '',
pathname: '/products/**',
search: '',
},
],
},
};
export default nextConfig;
官方文档现在更推荐 remotePatterns,而不是旧式 domains。新的方向更细,也更安全。
<Image /> 最常见的坑
1. 没有宽高
如果不是 fill 模式,就应该提供 width 和 height。
2. 远程图片白名单太宽
配置越泛,越容易引入滥用和安全边界问题。官方也强调要尽量写得具体。
3. 把所有首屏图都设成高优先级
较新的版本里,priority 已经走到废弃路线,新的方向是 preload。但本质没变:
- 只有真正关键的首屏图才值得提前加载
- 不要把整页所有图都当成关键资源
4. 还在使用 next/legacy/image
这通常说明项目还带着 13 之前的历史包袱,应该逐步迁到现在的 next/image。
next/font 解决什么问题
字体是性能里很容易被低估的一项。
官方的 next/font 主要做两件事:
- 自动优化字体加载
- 自动自托管字体文件,避免浏览器直接请求 Google Fonts
import { Geist } from 'next/font/google';
const geist = Geist({
subsets: ['latin'],
display: 'swap',
});
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en" className={geist.className}>
<body>{children}</body>
</html>
);
}
为什么 next/font 很值得默认采用
- 减少外部网络请求
- 隐私更好
- 更容易控制
display策略 - 更容易避免字体导致的布局跳动
本地字体怎么接
import localFont from 'next/font/local';
const brandFont = localFont({
src: './fonts/Brand-Regular.woff2',
display: 'swap',
variable: '--font-brand',
});
这在品牌项目、中文字体、自定义字库场景里很常见。
字体最常见的几个判断
全局统一字体
通常放根布局。
某个专题或品牌块单独字体
可以局部定义,避免把全站都绑 进去。
多字体系统
尽量明确层级和变量,不要让字体定义到处散落。
next/script 解决什么问题
第三方脚本经常是性能问题大户。next/script 的价值在于:
- 控制脚本什么时候加载
- 控制加载范围
- 避免在路由切换时重复插入同一脚本
import Script from 'next/script';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="zh-CN">
<body>{children}</body>
<Script src="https://example.com/analytics.js" />
</html>
);
}
Script 的四种策略
beforeInteractive
在任何 Next 代码和页面水合之前加载。
适合:
- 极早期必须可用的脚本
- 很少数必须抢在应用前执行的能力
afterInteractive
默认策略。页面发生一部分 hydration 后尽早加载。
适合:
- 分析脚本
- 常规第三方 SDK
lazyOnload
在浏览器空闲时加载。
适合:
- 非关键统计
- 不影响主流程的增强脚本
worker
实验态,把脚本放进 web worker。
但官方文档也明确提醒:
- 这还是实验能力
- 在 App Router 里还不能稳定使用
所以现在不适合作为默认方案。
脚本应该挂在哪
根布局
适合全站都要用的脚本。
某个子布局
适合某个业务区块才会用的脚本。
某个页面
适合范围非常明确的第三方能力。
官方也明确建议:尽量把第三方脚本限制在必要页面或布局,而不是全站一股脑注入。
最常见的第三方脚本问题
1. 脚本放太早
首屏被第三方拖慢。
2. 脚本挂太高
明明只有一个页面用,却放进全站布局。