Next.js: SEO 与 Metadata API
最后更新:2026-08-26
SEO 是网站的免费流量发动机——正确的 Metadata 能让 Google 收录效率提升 3 倍。
1. 你将学到
generateMetadata()动态生成页面 title / description / openGraph / twittermetadata静态导出与静态路由元数据- JSON-LD 结构化数据(BreadcrumbList / Article / FAQPage)
- Sitemap 与
robots.txt的动态生成 - hreflang 多语种 SEO 配置
@vercel/og+ Satori 动态生成 OG 图片
2. 一个内容运营经理的真实故事
(1) 痛点:上线 3 个月,Google 只收录了 5 页
Bob 负责 TaskFlow 的内容运营。博客上线 3 个月,写 50 篇文章:
"Google Search Console 显示只收录了 5 页。检查发现:所有页面
<title>都是 'TaskFlow',没有 meta description,没有 Open Graph 图片——社交分享时只有光秃秃的链接。"
| 问题 | 影响 | 量化 |
|---|---|---|
| 重复 title | 搜索引擎无法区分页面 | 收录仅 5/50 |
| 无 OG 图片 | 社交分享无预览图 | 分享点击率 0.3% |
| 无 JSON-LD | 无富媒体摘要 | 搜索结果无额外信息 |
| 无 Sitemap | Google 爬不到深层页面 | 收录延迟 30 天 |
(2) Next.js Metadata API 的解法
用
generateMetadata()为每个页面生成独特 SEO 标签 + JSON-LD 结构化数据。
TS
// app/blog/[slug]/page.tsx — 动态 SEO
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} - TaskFlow 博客`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [{ url: post.ogImage, width: 1200, height: 630 }]
}
}
}
(3) 收益
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| Google 收录率 | 10% | 98% | 9.8x |
| 社交分享点击率 | 0.3% | 2.8% | 9.3x |
| 搜索结果富媒体 | ❌ 无 | ✅ 面包屑 + 文章卡片 | — |
| 爬虫发现页速度 | 30 天 | < 24h | 30x |
3. 静态与动态 Metadata
(1) 静态导出
TS
// app/about/page.tsx — 静态页面 metadata
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: '关于 TaskFlow - 团队协作平台',
description: 'TaskFlow 帮助 10,000+ 团队高效协同工作,提供项目管理、实时协作和智能分析功能。',
keywords: ['团队协作', '项目管理', 'TaskFlow', 'SaaS'],
authors: [{ name: 'TaskFlow Team', url: 'https://taskflow.io' }]
}
(2) 动态生成
TS
// app/products/[id]/page.tsx — 动态 metadata
import type { Metadata, ResolvingMetadata } from 'next'
type Props = { params: Promise<{ id: string }>; searchParams: Promise<{ [key: string]: string | string[] | undefined }> }
export async function generateMetadata({ params, searchParams }: Props, parent: ResolvingMetadata): Promise<Metadata> {
const { id } = await params
const product = await fetch(`https://api.taskflow.io/products/${id}`).then(r => r.json())
return {
title: `${product.name} - TaskFlow 产品`,
description: product.description,
openGraph: {
title: product.name,
description: product.description,
url: `https://taskflow.io/products/${id}`,
siteName: 'TaskFlow',
images: [
{
url: product.ogImage,
width: 1200,
height: 630,
alt: product.name
}
],
locale: 'zh_CN',
type: 'website'
},
twitter: {
card: 'summary_large_image',
title: product.name,
description: product.description,
images: [product.ogImage]
},
alternates: {
canonical: `https://taskflow.io/products/${id}`,
languages: {
'en': `https://taskflow.io/en/products/${id}`,
'ja': `https://taskflow.io/ja/products/${id}`,
'ar': `https://taskflow.io/ar/products/${id}`
}
}
}
}
(3) Metadata 字段速查表
| 字段 | 作用 | 示例 |
|---|---|---|
title |
页面标题 / <title> |
'产品详情 - TaskFlow' |
description |
搜索引擎摘要 | 'TaskFlow 项目管理工具...' |
openGraph |
Facebook / LinkedIn 分享 | { title, description, images } |
twitter |
X (Twitter) 卡片 | { card: 'summary_large_image' } |
alternates.canonical |
权威 URL 防重复内容 | https://taskflow.io/page |
alternates.languages |
hreflang 多语种 | { 'en': '...' } |
robots |
爬虫指令 | { index: true, follow: true } |
▶ 示例:博客文章完整 SEO
TS
// app/blog/[slug]/page.tsx
import type { Metadata } from 'next'
interface Post {
title: string
excerpt: string
ogImage: string
publishedAt: string
author: string
tags: string[]
}
async function getPost(slug: string): Promise<Post> {
const res = await fetch(`https://api.taskflow.io/blog/${slug}`, { next: { revalidate: 3600 } })
return res.json()
}
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
const { slug } = await params
const post = await getPost(slug)
return {
title: `${post.title} | TaskFlow 博客`,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [{ url: post.ogImage, width: 1200, height: 630 }]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [post.ogImage]
},
keywords: [...post.tags, 'TaskFlow', '项目管理'],
alternates: { canonical: `https://taskflow.io/blog/${slug}` },
robots: { index: true, follow: true }
}
}
▶ 示例:使用 robots 元标签控制爬虫
TS
// app/private/dashboard/page.tsx — 禁止搜索引擎索引
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Dashboard - TaskFlow',
robots: {
index: false, // 不索引
follow: false, // 不追踪链接
noarchive: true, // 不缓存
nosnippet: true, // 不显示摘要
nocache: true // 不缓存
}
}
export default function DashboardPage() {
return <h1>私密仪表盘</h1>
}
💻 输出(HTML head):
HTML
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet" />
4. JSON-LD 结构化数据
(1) 三种常见 Schema
graph TB
A[JSON-LD 结构化数据] --> B[BreadcrumbList<br/>面包屑导航]
A --> C[Article<br/>文章详情]
A --> D[FAQPage<br/>常见问题]
B --> E[搜索结果显示路径]
C --> F[知识面板 + 封面图]
D --> G[搜索结果直接显示问答]
style A fill:#cce5ff
style B fill:#d4edda
style C fill:#d4edda
style D fill:#d4edda
| Schema | 使用场景 | 搜索结果表现 |
|---|---|---|
| BreadcrumbList | 所有页面 | 显示面包屑路径(首页 > 产品 > 详情) |
| Article | 博客文章 | 文章卡片(标题 + 摘要 + 封面 + 发布时间) |
| FAQPage | 帮助中心 / 常见问题 | 显示问答列表,可折叠 |
| Product | 产品页 | 价格 + 库存 + 评分星星 |
(2) JSON-LD 注入函数
TS
// lib/jsonld.ts — JSON-LD 生成工具
export function breadcrumbJsonld(items: { name: string; url: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url
}))
}
}
export function articleJsonld(post: {
title: string
excerpt: string
url: string
ogImage: string
publishedAt: string
author: string
}) {
return {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
image: post.ogImage,
datePublished: post.publishedAt,
author: { '@type': 'Person', name: post.author },
publisher: { '@type': 'Organization', name: 'TaskFlow', logo: 'https://taskflow.io/logo.png' },
mainEntityOfPage: { '@type': 'WebPage', '@id': post.url }
}
}
▶ 示例:文章页 JSON-LD + Breadcrumb
TSX
// app/blog/[slug]/page.tsx — 完整的 SEO + JSON-LD
import { breadcrumbJsonld, articleJsonld } from '@/lib/jsonld'
export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
const post = await getPost(slug)
const url = `https://taskflow.io/blog/${slug}`
const breadcrumb = breadcrumbJsonld([
{ name: '首页', url: 'https://taskflow.io' },
{ name: '博客', url: 'https://taskflow.io/blog' },
{ name: post.title, url }
])
const article = articleJsonld({
title: post.title,
excerpt: post.excerpt,
url,
ogImage: post.ogImage,
publishedAt: post.publishedAt,
author: post.author
})
return (
<>
{/* JSON-LD 注入 */}
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(breadcrumb) }}
/>
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(article) }}
/>
<article>
<h1>{post.title}</h1>
<p>{post.excerpt}</p>
<div>{post.content}</div>
</article>
</>
)
}
💡 提示: Google Search Console 提供 "Rich Results Test" 工具,可验证 JSON-LD 是否正确解析。
5. Sitemap 与 robots.txt
(1) 动态 Sitemap 生成
TS
// app/sitemap.ts — 动态 Sitemap(自动包含所有页面)
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://taskflow.io'
// 静态页面
const staticPages = [
{ url: baseUrl, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 1.0 },
{ url: `${baseUrl}/about`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
{ url: `${baseUrl}/blog`, lastModified: new Date(), changeFrequency: 'weekly' as const, priority: 0.9 },
{ url: `${baseUrl}/pricing`, lastModified: new Date(), changeFrequency: 'monthly' as const, priority: 0.8 },
{ url: `${baseUrl}/contact`, lastModified: new Date(), changeFrequency: 'yearly' as const, priority: 0.5 }
]
// 动态博客文章(从 API 获取)
const posts = await fetch('https://api.taskflow.io/blog/posts').then(r => r.json())
const blogPages = posts.map((post: { slug: string; updatedAt: string }) => ({
url: `${baseUrl}/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.7
}))
// 多语种页面
const locales = ['en', 'ja', 'ar']
const localizedPages = locales.flatMap((locale) =>
staticPages.map((page) => ({
url: `${baseUrl}/${locale}${page.url.replace(baseUrl, '')}`,
lastModified: page.lastModified,
changeFrequency: page.changeFrequency,
priority: page.priority * 0.9,
alternates: {
languages: Object.fromEntries(
locales.map((l) => [l, `${baseUrl}/${l}${page.url.replace(baseUrl, '')}`])
)
}
}))
)
return [...staticPages, ...blogPages, ...localizedPages]
}
(2) robots.txt
TS
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
disallow: ['/api/', '/admin/', '/_next/', '/dashboard']
},
{
userAgent: 'GPTBot',
disallow: '/'
}
],
sitemap: 'https://taskflow.io/sitemap.xml'
}
}
▶ 示例:多语种 Sitemap 验证
TEXT
📖 仅展示
# 生成的 sitemap.xml 片段
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
xmlns:xhtml="http://www.w3.org/1999/xhtml">
<url>
<loc>https://taskflow.io/about</loc>
<lastmod>2026-07-06</lastmod>
<changefreq>monthly</changefreq>
<priority>0.8</priority>
<xhtml:link rel="alternate" hreflang="en" href="https://taskflow.io/en/about"/>
<xhtml:link rel="alternate" hreflang="ja" href="https://taskflow.io/ja/about"/>
<xhtml:link rel="alternate" hreflang="ar" href="https://taskflow.io/ar/about"/>
</url>
</urlset>
6. 动态 OG 图片生成
(1) @vercel/og 架构
graph LR
A[分享链接到社交媒体] --> B[爬虫请求 OG 图片]
B --> C[@vercel/og Edge Function]
C --> D[Satori + React]
D --> E[将 JSX 渲染为 PNG]
E --> F[1200×630 OG 图片]
F --> G[Facebook / X / LinkedIn 显示]
style C fill:#cce5ff
style D fill:#d4edda
style E fill:#fff3cd
| 库 | 作用 | 说明 |
|---|---|---|
@vercel/og |
OG 图片生成 Edge Function | 零服务器开销 |
Satori |
JSX → SVG 转换 | 不到 1ms |
resvg-wasm |
SVG → PNG 渲染 | ~5ms |
(2) 安装与基础示例
BASH
npm install @vercel/og
TSX
// app/og/route.tsx — OG 图片生成 API
import { ImageResponse } from '@vercel/og'
export const runtime = 'edge'
export async function GET() {
return new ImageResponse(
(
<div style={{
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
background: 'linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%)',
color: 'white',
fontSize: 60,
fontWeight: 700,
padding: 40
}}>
<h1>TaskFlow</h1>
<p style={{ fontSize: 32, opacity: 0.9 }}>团队协作平台</p>
</div>
),
{ width: 1200, height: 630 }
)
}
▶ 示例:动态文章 OG 图片
TSX
// app/blog/[slug]/og/route.tsx — 动态文章 OG 图片
import { ImageResponse } from '@vercel/og'
export const runtime = 'edge'
export async function GET(req: Request, { params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params
// 获取文章信息
const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())
return new ImageResponse(
(
<div style={{
width: 1200,
height: 630,
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
padding: 60,
background: 'linear-gradient(135deg, #1e1b4b 0%, #4f46e5 100%)',
color: 'white'
}}>
{/* 标签 */}
<div style={{ display: 'flex', gap: 8 }}>
{post.tags?.slice(0, 3).map((tag: string) => (
<span key={tag} style={{
padding: '4px 12px',
borderRadius: 20,
background: 'rgba(255,255,255,0.2)',
fontSize: 18
}}>{tag}</span>
))}
</div>
{/* 标题 */}
<h1 style={{ fontSize: 52, margin: '20px 0', lineHeight: 1.2 }}>
{post.title}
</h1>
{/* 作者 + 日期 */}
<div style={{ display: 'flex', gap: 16, fontSize: 22, opacity: 0.8 }}>
<span>{post.author}</span>
<span>{new Date(post.publishedAt).toLocaleDateString('zh-CN')}</span>
</div>
{/* Logo */}
<div style={{ position: 'absolute', bottom: 40, right: 60, fontSize: 28, fontWeight: 'bold' }}>
TaskFlow Blog
</div>
</div>
),
{ width: 1200, height: 630 }
)
}
💡 提示: OG 图片 Route Handler 在
/blog/[slug]/og/route.tsx,最终链接为 https://taskflow.io/blog/nextjs-seo-guide/og。在 generateMetadata 中引用此地址即可。
7. 完整示例:多语种 SEO + OG 图片综合实现
TSX
// app/layout.tsx — 根布局默认 SEO + 多语种
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: { template: '%s | TaskFlow', default: 'TaskFlow - 团队协作平台' },
description: 'TaskFlow 帮助全球 10,000+ 团队高效协同工作。提供项目管理、实时协作、AI 智能分析。',
openGraph: {
siteName: 'TaskFlow',
type: 'website',
locale: 'zh_CN',
images: [{ url: 'https://taskflow.io/og-default.png', width: 1200, height: 630 }]
},
twitter: { card: 'summary_large_image', site: '@taskflow' },
robots: { index: true, follow: true },
alternates: {
canonical: 'https://taskflow.io',
languages: {
'en': 'https://taskflow.io/en',
'ja': 'https://taskflow.io/ja',
'ar': 'https://taskflow.io/ar'
}
}
}
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>{children}</body>
</html>
)
}
TS
// app/sitemap.ts — 完整 Sitemap
import type { MetadataRoute } from 'next'
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const baseUrl = 'https://taskflow.io'
const locales = ['zh', 'en', 'ja', 'ar'] as const
const pages = ['', '/about', '/blog', '/pricing', '/contact']
return pages.flatMap((page) =>
locales.map((locale) => ({
url: `${baseUrl}/${locale}${page}`,
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: page === '' ? 1.0 : 0.8,
alternates: {
languages: Object.fromEntries(locales.map((l) => [l, `${baseUrl}/${l}${page}`]))
}
}))
)
}
TS
// app/robots.ts
import type { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: { userAgent: '*', allow: '/', disallow: ['/api/', '/admin/', '/dashboard'] },
sitemap: 'https://taskflow.io/sitemap.xml'
}
}
TSX
// app/blog/[slug]/page.tsx — 文章页整合所有 SEO
import type { Metadata } from 'next'
type Props = { params: Promise<{ slug: string }> }
export async function generateMetadata({ params }: Props): Promise<Metadata> {
const { slug } = await params
const post = await fetch(`https://api.taskflow.io/blog/${slug}`).then(r => r.json())
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: 'article',
publishedTime: post.publishedAt,
authors: [post.author],
images: [{ url: `https://taskflow.io/blog/${slug}/og`, width: 1200, height: 630 }]
},
twitter: {
card: 'summary_large_image',
title: post.title,
description: post.excerpt,
images: [`https://taskflow.io/blog/${slug}/og`]
},
alternates: {
canonical: `https://taskflow.io/blog/${slug}`,
languages: {
en: `https://taskflow.io/en/blog/${slug}`,
ja: `https://taskflow.io/ja/blog/${slug}`,
ar: `https://taskflow.io/ar/blog/${slug}`
}
}
}
}
export default async function BlogPostPage({ params }: Props) {
const { slug } = await params
return (
<article>
{/* JSON-LD 结构化数据 */}
<script type="application/ld+json" dangerouslySetInnerHTML={{
__html: JSON.stringify({
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{ '@type': 'ListItem', position: 1, name: '首页', item: 'https://taskflow.io' },
{ '@type': 'ListItem', position: 2, name: '博客', item: 'https://taskflow.io/blog' },
{ '@type': 'ListItem', position: 3, name: slug }
]
})
}} />
<h1>文章标题</h1>
<div>文章内容...</div>
</article>
)
}
❓ 常见问题
Q
generateMetadata() 和 metadata 导出有什么区别?A
metadata 是静态导出,用于静态路由(如 /about)。generateMetadata() 是异步函数,根据 params / searchParams 动态生成 Metadata,用于动态路由(如 /blog/[slug])。两者不能同时使用。Q JSON-LD 和 meta tags 是什么关系?
A meta tags(
<title> / <meta name="description">)是基础 SEO 标签。JSON-LD(<script type="application/ld+json">)是结构化数据,让 Google 显示富媒体摘要(面包屑、星级评分、FAQ 折叠)。两者互补,都需要实现。Q 动态 OG 图片生成是否会增加服务器负担?
A
@vercel/og 运行在 Edge Runtime(边缘计算),每张图片生成只需 ~5ms,几乎是零开销。生产环境建议开启 CDN 缓存(Cache-Control: public, max-age=31536000, immutable)。Q hreflang 标签应该在什么位置配置?
A 在
generateMetadata() 的 alternates.languages 配置,Next.js 自动注入 <link rel="alternate" hreflang="en" href="..."> 到页面 <head>。同时在 Sitemap 中也需要包含 xhtml:link 关联标签。Q Sitemap 中的
changeFrequency 和 priority 对 Google 排名有影响吗?A Google 官方表示已忽略这两个字段。它们对 Bing / Yandex 等搜索引擎仍有参考价值。建议 Sitemap 包含所有页面,且
lastModified 准确——这对爬虫判断是否重新爬取很重要。📖 小节
generateMetadata()实现动态 SEO,支持 title / description / openGraph / twitter 全字段- JSON-LD 结构化数据(BreadcrumbList / Article / FAQPage)让搜索结果显示富媒体摘要
- Sitemap 和 robots.txt 通过
app/sitemap.ts/app/robots.ts动态生成,支持多语种替代链接 - hreflang 多语种 SEO 在
alternates.languages配置,确保搜索引擎把正确语言的页面展示给对应地区用户 @vercel/og+ Satori 在 Edge Runtime 动态生成 1200×630 的 OG 图片,每篇文章可拥有独特分享预览- SEO 综合策略:Metadata(基础) + JSON-LD(增强) + Sitemap(发现) + OG 图片(社交)
📝 作业
-
基础题(⭐):在静态路由
/about中配置metadata导出,包含 title、description、openGraph 和 twitter 卡片。 -
进阶题(⭐⭐):实现一个新闻详情页的完整 SEO:
generateMetadata()动态生成 title/description/OG/twitter,注入 Article JSON-LD 结构化数据,并在 Sitemap 中列出所有文章 URL。 -
挑战题(⭐⭐⭐):搭建一个完整的 OG 图片生成系统:
@vercel/og为每篇博客文章生成带标题、作者、标签的 1200×630 分享图,在generateMetadata()中引用该图片 URL,最后用 Google Rich Results Test 验证 JSON-LD 和 OG 标签的正确性。