Next.js 13 引入 App Router 后,元数据管理方式发生了根本性变化。在 Pages Router 时代,开发者习惯在页面组件里直接使用 next/head 动态插入 <title> 和 <meta> 标签;但 App Router 中的页面默认是服务端组件,<head> 标签不再由组件在渲染期间随意修改。Next.js 为此提供了 metadata 对象导出与 generateMetadata 函数两种方式,统一处理 SEO 信息。理解这两者的执行边界、合并策略以及如何与路由参数和异步数据配合,是实现动态 SEO 元数据管理的第一步。

区分 metadata 静态导出与 generateMetadata 动态生成
静态 metadata 导出适合那些不需要请求数据就能确定的元信息,例如站点名称、默认描述或全站通用的 Open Graph 图片。它只能写在服务端组件中,客户端组件无法导出 metadata。在 layout.tsx 或 page.tsx 文件顶部直接导出一个满足 Metadata 类型的对象,Next.js 会在服务端渲染时自动将其转换为 <head> 中对应的 <title>、<meta name="description"> 以及 <meta property="og:title"> 等标签。
import type { Metadata } from 'next';
export const metadata: Metadata = {
title: '站点名称',
description: '这是默认的站点描述',
};
generateMetadata 则更加灵活,它可以是同步函数,也可以是异步函数,并且能够接收当前路由的 params 和 searchParams 参数。它的返回值类型与静态 metadata 完全一致,但优先级更高:如果页面中同时存在静态 metadata 和 generateMetadata,后者返回的字段会覆盖前者的同名字段,未覆盖的字段则继续保留。这种合并机制允许你在根布局中放置全局默认标题和描述,而在具体页面中只覆盖需要变更的部分,避免重复配置。
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await fetchPost(params.slug);
return {
title: post.title,
description: post.excerpt,
};
}
需要特别注意的是,generateMetadata 同样只能在服务端组件中使用。它要么在构建阶段执行,要么在每次请求到达服务端时执行,因此无法访问浏览器的 window 对象或任何客户端状态。如果你的页面包含客户端交互并且需要根据客户端状态动态更新标题,正确的做法是将客户端组件包裹在一个服务端组件中,由服务端组件负责导出 metadata 或 generateMetadata,再把必要的数据通过 props 传给客户端组件。直接在客户端组件里操作 document.title 虽然肉眼可见,但搜索引擎爬虫往往不会执行那段 JavaScript,最终抓到的仍是服务端输出的默认值。
基于路由参数和搜索参数生成动态标题与描述
博客文章页 /blog/[slug]、商品详情页 /products/[id] 以及带筛选条件的列表页是最常见的动态 SEO 场景。通过 generateMetadata 的 params 参数可以拿到当前路由的动态段值,通过 searchParams 参数可以读取查询字符串。在 Next.js 15 中,这两个参数可能是 Promise 对象,为了兼容不同版本,建议统一使用 await 进行解包。这样即使未来升级到异步 API,代码也不会因为同步取值而报错。
export async function generateMetadata({ params, searchParams }: {
params: { slug: string };
searchParams: { [key: string]: string | string[] | undefined };
}): Promise<Metadata> {
const resolvedParams = await params;
const resolvedSearchParams = await searchParams;
const slug = resolvedParams.slug;
const page = resolvedSearchParams.page ?? '1';
return {
title: `文章 ${slug} - 第 ${page} 页`,
description: `阅读关于 ${slug} 的详细内容`,
};
}
如果页面组件本身已经通过 fetch 获取了文章内容或商品数据,而 generateMetadata 又再次请求相同接口,就会产生重复网络请求,增加首屏响应时间。解决方法是使用 React 提供的 cache 函数将数据请求包裹起来,让 page 组件和 generateMetadata 共享同一个缓存实例。在同一请求周期内,cache 会保证函数只执行一次,后续调用直接返回缓存结果。
import { cache } from 'react';
export const getPost = cache(async (slug: string) => {
const res = await fetch(`https://api.ipipp.com/posts/${slug}`);
return res.json();
});
在 page.tsx 中调用 getPost(params.slug) 渲染页面正文,在 generateMetadata 中同样调用 getPost(params.slug) 获取标题和描述。由于 cache 的存在,这两个调用只会触发一次真实的网络请求。这样做不仅提升了性能,还能保证元数据中的标题与页面正文数据来自同一份数据源,避免出现标题显示 A 文章而正文却是 B 文章的不一致问题。
动态 Open Graph、Twitter Card 与 robots 配置
搜索引擎优化不止包含 <title> 和 <meta name="description">。当链接被分享到社交平台时,Open Graph 和 Twitter Card 决定了卡片预览的标题、描述和封面图。在 generateMetadata 的返回对象中可以直接声明 openGraph 与 twitter 字段,Next.js 会自动生成对应的 <meta> 标签,无需手动拼接字符串。
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await getPost(params.slug);
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
url: `https://ipipp.com/posts/${post.slug}`,
images: [{ url: post.coverImage, width: 1200, height: 630, alt: post.title }],
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
};
}
如果只想在 Twitter Card 中复用 Open Graph 的图片和描述,twitter 字段可以只写 card,其余字段会从 openGraph 中自动继承。但要注意,某些平台对 og:image 的尺寸和格式有严格要求,建议始终提供宽高字段,并使用绝对 URL 而不是相对路径。
对于搜索引擎抓取策略,metadata 对象中的 robots 字段可以控制 index、follow、noarchive 等行为。在需要动态决定某一页是否收录时,可以在 generateMetadata 中根据业务逻辑返回不同的 robots 配置。如果想要在站点级别统一声明 robots 规则或关联 sitemap 地址,则可以使用 App Router 提供的 robots.ts 文件。两者并不冲突:robots.ts 负责全局默认值,页面级 metadata.robots 负责覆盖特殊页面的行为。配合 sitemap.ts 动态生成 sitemap,可以形成完整的 SEO 闭环,帮助搜索引擎更高效地发现和索引路由。
避免元数据闪变与常见调试方法
元数据闪变指的是页面在客户端导航或水合完成后,标题和描述短暂显示为默认值,随后才被替换为目标内容。这种问题通常发生在数据获取被放在客户端 useEffect 中,或者 generateMetadata 没有与页面组件共享请求缓存导致数据返回时间不一致。要避免闪变,核心原则是将动态 SEO 所需的数据获取全部提升到服务端组件中,并通过 cache 函数缓存请求结果。这样服务端渲染出的 HTML 一开始就包含正确的 <title> 和 <meta> 标签,客户端水合时无需再修正。
另一个常见误区是只通过浏览器开发者工具查看 DOM 中的 <head> 内容,以此判断 SEO 标签是否正确。实际上,客户端组件动态插入的标签虽然会显示在 DOM 中,但搜索引擎爬虫在抓取原始 HTML 时可能看不到它们。正确的调试方式是使用浏览器“查看源代码”功能,或者通过 curl 命令直接获取页面初始 HTML,检查 <meta> 标签是否真实存在于服务端输出中。如果源代码里缺少预期标签,说明 generateMetadata 没有被正确执行或数据返回为空。
动态 SEO 元数据管理并不是一个孤立的知识点,它要求开发者理解 App Router 的服务端组件模型、路由参数解析、请求缓存以及元数据合并优先级。把数据获取逻辑抽离为可缓存的函数,并让 page 组件与 generateMetadata 共用该函数,是保证一致性和性能的关键。在需要动态控制社交分享卡片、robots 策略和 sitemap 时,Next.js 提供的 API 已经足够覆盖绝大多数业务场景,不必退回到手动操作 document.title 的老路。
Next.js App Router动态SEO元数据管理修改时间:2026-08-22 16:08:08