本节摘要:内容型网站的命脉是搜索引擎收录。Next.js 的 Metadata API 让你在组件里声明页面的 title、description、OG 标签、结构化数据。本节讲清静态与动态元数据、generateMetadata 的用法、sitemap 与 robots 的生成,以及 SEO 的核心实践。
阅读完本节,你应当能够:
搜索引擎爬虫看到的是HTML 源码。纯 React(CSR)的源码是空壳,爬虫拿不到内容;Next.js 默认服务器渲染,源码里就有完整内容与 title/description——这是 Next.js 在内容型网站领域碾压纯 SPA 的根本原因。
直觉类比:SEO 像"图书馆编目"——title 是书名、description 是内容简介、OG 标签是"社交平台上的封面"。编目做得好,用户(搜索引擎/社交平台)才愿意展示你的页面。
💡 关键直觉:Metadata API 是"声明式 SEO"——不用手动写
<head>里的 title/meta 标签,在组件里导出 metadata 对象,Next.js 自动注入到 HTML 头部,SSR 渲染时爬虫就能读到。
// app/about/page.tsx import type { Metadata } from "next"; export const metadata: Metadata = { title: "关于我们 | 我的网站", description: "介绍我们的团队与使命", keywords: ["Next.js", "React", "全栈"], }; export default function AboutPage() { return <h1>关于我们</h1>; }
title 模板:在根布局定义统一后缀:
// app/layout.tsx export const metadata: Metadata = { title: { default: "我的网站", template: "%s | 我的网站", // 子页面 title 自动加后缀 }, description: "网站描述", };
需要读取路由参数/数据后生成:
// app/posts/[slug]/page.tsx 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); // 按 slug 取文章 return { title: post.title, description: post.excerpt, }; }
关键:generateMetadata 在服务器运行,可以访问数据——每篇文章自动拥有自己的 title/description。
export const metadata: Metadata = { title: "文章标题", openGraph: { title: "文章标题", description: "分享到微信/推特时显示的描述", images: ["/og-image.png"], // 分享封面图 type: "article", }, twitter: { card: "summary_large_image", title: "文章标题", images: ["/og-image.png"], }, };
为什么重要:用户把链接发到微信/微博/推特时,展示卡片(标题+描述+图)而非裸链接——分享转化率的隐形推手。
// app/sitemap.ts import type { MetadataRoute } from "next"; export default function sitemap(): MetadataRoute.Sitemap { return [ { url: "https://example.com", lastModified: new Date() }, { url: "https://example.com/about", lastModified: new Date() }, { url: "https://example.com/blog", lastModified: new Date() }, ]; }
动态数据(文章列表)可生成完整 sitemap——告诉搜索引擎"你有哪些页面"。
// app/robots.ts import type { MetadataRoute } from "next"; export default function robots(): MetadataRoute.Robots { return { rules: [ { userAgent: "*", allow: "/", disallow: ["/admin/", "/api/"] }, ], sitemap: "https://example.com/sitemap.xml", }; }
控制爬虫可访问范围,避免后台被收录。
| 项目 | 做法 |
|---|---|
| 每个页面有独立 title/description | metadata 或 generateMetadata |
| 语义化 HTML | 用 h1/h2、article、nav 标签 |
| 图片有 alt | 无障碍 + 图片 SEO |
| 结构化数据 | JSON-LD(文章、产品、FAQ) |
| 规范 URL | canonical 防重复收录 |
| 响应式 | 移动优先(Google 标准) |
| 误区 | 现象 | 正解 |
|---|---|---|
| 忘 export metadata | 页面 title 是默认 | 每个页面导出 metadata |
| 动态页面 title 固定 | 所有文章同标题 | generateMetadata 按参数生成 |
| 忘设置 OG | 分享无卡片 | openGraph 配置 |
| 后台被收录 | 隐私泄露 | robots 禁止 + noindex |
| sitemap 404 | 文件命名错 | app/sitemap.ts 自动生成 /sitemap.xml |
// app/posts/[slug]/page.tsx —— 完整 SEO import type { Metadata } from "next"; async function getPost(slug: string) { // 模拟取数据 return { title: `文章 ${slug}`, excerpt: "摘要内容", content: "正文..." }; } 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.excerpt, openGraph: { title: post.title, description: post.excerpt, type: "article", }, alternates: { canonical: `/posts/${slug}` }, }; } export default async function PostPage({ params, }: { params: Promise<{ slug: string }>; }) { const { slug } = await params; const post = await getPost(slug); return ( <article> <h1>{post.title}</h1> <p>{post.excerpt}</p> <div>{post.content}</div> </article> ); }
验证:访问文章页,右键查看网页源代码——title/description/OG/canonical 全部在 HTML 源码中(爬虫可见)——这就是 Next.js 的 SEO 优势落地。
关键:generateMetadata 在服务器运行、可访问数据——每篇文章的标题、描述都按内容动态生成,而不是所有页面共用一个。
export default function ArticlePage() { const jsonLd = { "@context": "https://schema.org", "@type": "Article", headline: post.title, datePublished: post.createdAt, author: { "@type": "Person", name: post.author.name }, }; return ( <> <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} /> <article>{/* ... */}</article> </> ); }
一句话:SEO 不是"上线后做的事",而是"每个页面生成时就该带的属性"——Metadata API 让这件事成为组件声明的一部分,天然随代码演进。
问:generateMetadata 和 metadata 能同时用吗?
同页面两者取其所需:有动态数据用 generateMetadata(覆盖静态),无数据用静态 metadata。generateMetadata 优先。
问:怎么看 SEO 是否生效?
访问页面后查看网页源代码(Ctrl+U),搜索 title、meta description、og: 标签是否在 HTML 中。爬虫看到的就是源码内容。
问:多语言站点 SEO 怎么做?
每个语言页面独立 URL(/zh、/en),generateMetadata 里配 alternates.languages(hreflang)告诉搜索引擎语言版本关系。
问:需要给每个页面都写 description 吗?
建议写。description 影响搜索结果的点击率(虽不直接影响排名)。没有时搜索引擎会截取正文,效果差。
问:sitemap 需要手动更新吗?
动态站点建议在 sitemap.ts 里从数据库生成文章列表,随内容自动更新;纯静态站点构建时生成一次即可。
SEO 的落地方式是"每个页面生成时自带元数据"——静态页面用 export metadata,动态页面用 generateMetadata(读取参数与数据)。再加 sitemap 与 robots,内容型网站的收录基础就齐了。验证方式:查看网页源码,title/description/OG 标签都在 HTML 里。
做一个"文章列表 + 详情"的 SEO 完整配置:列表页用静态 metadata(带 title 模板后缀),详情页用 generateMetadata(从 URL 参数取文章数据生成 title/description/OG)。查看源码确认每个页面都有独立标签。再加 sitemap.ts(含动态文章 URL)与 robots.ts(禁止 admin 路径)。
进阶练习:在详情页加 JSON-LD 结构化数据(Article 类型),用 Google 的结构化数据测试工具验证;给分享场景配好 og:image,在微信开发者工具里预览分享卡片。SEO 不是玄学——每项配置都能在源码或工具里验证。
能说出静态 metadata 与 generateMetadata 的适用场景区别吗?能说出 hreflang 的作用吗?能说出验证 SEO 生效的方法吗?全部答对,SEO 基础就掌握了。