Next.js: SEO 与 Metadata API

最后更新:2026-08-26

SEO 是网站的免费流量发动机——正确的 Metadata 能让 Google 收录效率提升 3 倍。

1. 你将学到


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

100%
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 架构

100%
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 标签应该在什么位置配置?
AgenerateMetadata()alternates.languages 配置,Next.js 自动注入 <link rel="alternate" hreflang="en" href="..."> 到页面 <head>。同时在 Sitemap 中也需要包含 xhtml:link 关联标签。
Q Sitemap 中的 changeFrequencypriority 对 Google 排名有影响吗?
A Google 官方表示已忽略这两个字段。它们对 Bing / Yandex 等搜索引擎仍有参考价值。建议 Sitemap 包含所有页面,且 lastModified 准确——这对爬虫判断是否重新爬取很重要。

📖 小节


📝 作业

  1. 基础题(⭐):在静态路由 /about 中配置 metadata 导出,包含 title、description、openGraph 和 twitter 卡片。

  2. 进阶题(⭐⭐):实现一个新闻详情页的完整 SEO:generateMetadata() 动态生成 title/description/OG/twitter,注入 Article JSON-LD 结构化数据,并在 Sitemap 中列出所有文章 URL。

  3. 挑战题(⭐⭐⭐):搭建一个完整的 OG 图片生成系统:@vercel/og 为每篇博客文章生成带标题、作者、标签的 1200×630 分享图,在 generateMetadata() 中引用该图片 URL,最后用 Google Rich Results Test 验证 JSON-LD 和 OG 标签的正确性。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏