Next.js 元数据管理 🏷️

🎯 概述

Next.js 提供了强大的元数据 API,用于定义应用程序的元数据(如 titledescription、Open Graph 标签等),优化 SEO 和社交媒体分享效果。

📦 为什么需要元数据管理?

元数据的重要性

  1. SEO 优化:帮助搜索引擎理解页面内容
  2. 社交分享:优化在社交媒体上的展示效果
  3. 用户体验:提供准确的页面标题和描述
  4. 品牌识别:统一的图标和主题色

🚀 基础用法

静态元数据

// app/page.tsx
import { Metadata } from 'next'

export const metadata: Metadata = {
  title: '首页',
  description: '欢迎来到我的网站',
}

export default function Page() {
  return <h1>首页内容</h1>
}

动态元数据

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

type Props = {
  params: { slug: string }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const post = await getPost(params.slug)
  
  return {
    title: post.title,
    description: post.excerpt,
  }
}

export default function Page({ params }: Props) {
  return <article>...</article>
}

📝 元数据字段

基本字段

export const metadata: Metadata = {
  // 页面标题
  title: '我的网站',
  
  // 页面描述
  description: '这是一个很棒的网站',
  
  // 关键词
  keywords: ['Next.js', 'React', 'TypeScript'],
  
  // 作者
  authors: [{ name: '张三', url: 'https://example.com' }],
  
  // 创建者
  creator: '张三',
  
  // 发布者
  publisher: '我的公司',
  
  // 版权信息
  copyright: '© 2024 我的公司',
  
  // 语言
  alternates: {
    canonical: 'https://example.com',
    languages: {
      'en-US': 'https://example.com/en',
      'zh-CN': 'https://example.com/zh',
    },
  },
}

标题模板

// app/layout.tsx
export const metadata: Metadata = {
  title: {
    template: '%s | 我的网站',
    default: '我的网站',
  },
}
// app/blog/page.tsx
export const metadata: Metadata = {
  title: '博客', // 最终显示:博客 | 我的网站
}

🌐 Open Graph 元数据

基本 Open Graph

export const metadata: Metadata = {
  openGraph: {
    title: '我的网站',
    description: '这是一个很棒的网站',
    url: 'https://example.com',
    siteName: '我的网站',
    locale: 'zh_CN',
    type: 'website',
    images: [
      {
        url: 'https://example.com/og-image.jpg',
        width: 1200,
        height: 630,
        alt: '网站预览图',
      },
    ],
  },
}

文章类型

export const metadata: Metadata = {
  openGraph: {
    type: 'article',
    title: '文章标题',
    description: '文章描述',
    publishedTime: '2024-01-01T00:00:00.000Z',
    modifiedTime: '2024-01-02T00:00:00.000Z',
    authors: ['张三', '李四'],
    tags: ['Next.js', 'React'],
    images: [
      {
        url: 'https://example.com/article-image.jpg',
        width: 1200,
        height: 630,
      },
    ],
  },
}

🐦 Twitter 卡片

Summary Card

export const metadata: Metadata = {
  twitter: {
    card: 'summary',
    title: '我的网站',
    description: '这是一个很棒的网站',
    creator: '@myhandle',
    images: ['https://example.com/twitter-image.jpg'],
  },
}

Large Image Card

export const metadata: Metadata = {
  twitter: {
    card: 'summary_large_image',
    title: '文章标题',
    description: '文章描述',
    creator: '@myhandle',
    images: {
      url: 'https://example.com/large-image.jpg',
      alt: '文章预览图',
    },
  },
}

🎨 图标和主题

Favicon

// app/layout.tsx
export const metadata: Metadata = {
  icons: {
    icon: '/favicon.ico',
    shortcut: '/favicon-16x16.png',
    apple: '/apple-touch-icon.png',
    other: {
      rel: 'apple-touch-icon-precomposed',
      url: '/apple-touch-icon-precomposed.png',
    },
  },
}

多尺寸图标

export const metadata: Metadata = {
  icons: {
    icon: [
      { url: '/icon-16x16.png', sizes: '16x16', type: 'image/png' },
      { url: '/icon-32x32.png', sizes: '32x32', type: 'image/png' },
    ],
    apple: [
      { url: '/apple-icon-57x57.png', sizes: '57x57' },
      { url: '/apple-icon-72x72.png', sizes: '72x72' },
    ],
  },
}

主题颜色

export const metadata: Metadata = {
  themeColor: [
    { media: '(prefers-color-scheme: light)', color: '#ffffff' },
    { media: '(prefers-color-scheme: dark)', color: '#000000' },
  ],
}

🤖 Robots 和索引

Robots 配置

export const metadata: Metadata = {
  robots: {
    index: true,
    follow: true,
    nocache: false,
    googleBot: {
      index: true,
      follow: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
}

阻止索引

// app/admin/page.tsx
export const metadata: Metadata = {
  robots: {
    index: false,
    follow: false,
  },
}

📱 移动端优化

Viewport

// app/layout.tsx
export const metadata: Metadata = {
  viewport: {
    width: 'device-width',
    initialScale: 1,
    maximumScale: 1,
    userScalable: false,
  },
}
export const metadata: Metadata = {
  appleWebApp: {
    capable: true,
    title: '我的应用',
    statusBarStyle: 'black-translucent',
  },
}

🔄 动态生成元数据

从 API 获取数据

// app/products/[id]/page.tsx
import { Metadata } from 'next'

type Props = {
  params: { id: string }
  searchParams: { [key: string]: string | string[] | undefined }
}

export async function generateMetadata({ params }: Props): Promise<Metadata> {
  const product = await fetch(`https://api.example.com/products/${params.id}`)
    .then((res) => res.json())

  return {
    title: product.name,
    description: product.description,
    openGraph: {
      title: product.name,
      description: product.description,
      images: [
        {
          url: product.image,
          width: 1200,
          height: 630,
        },
      ],
    },
  }
}

export default function Page({ params }: Props) {
  return <div>产品详情</div>
}

继承父级元数据

// app/blog/layout.tsx
export const metadata: Metadata = {
  title: {
    template: '%s | 博客',
    default: '博客',
  },
  description: '我的博客文章',
}

// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const post = await getPost(params.slug)
  
  return {
    title: post.title, // 会使用父级的 template
    description: post.excerpt,
  }
}

🎯 实战示例

完整的博客文章元数据

// app/blog/[slug]/page.tsx
import { Metadata } from 'next'

interface Post {
  title: string
  excerpt: string
  content: string
  author: string
  publishedAt: string
  updatedAt: string
  coverImage: string
  tags: string[]
}

async function getPost(slug: string): Promise<Post> {
  const res = await fetch(`https://api.example.com/posts/${slug}`)
  return res.json()
}

export async function generateMetadata({
  params,
}: {
  params: { slug: string }
}): Promise<Metadata> {
  const post = await getPost(params.slug)
  const baseUrl = 'https://example.com'

  return {
    title: post.title,
    description: post.excerpt,
    authors: [{ name: post.author }],
    keywords: post.tags,
    
    openGraph: {
      type: 'article',
      title: post.title,
      description: post.excerpt,
      url: `${baseUrl}/blog/${params.slug}`,
      siteName: '我的博客',
      locale: 'zh_CN',
      publishedTime: post.publishedAt,
      modifiedTime: post.updatedAt,
      authors: [post.author],
      tags: post.tags,
      images: [
        {
          url: post.coverImage,
          width: 1200,
          height: 630,
          alt: post.title,
        },
      ],
    },
    
    twitter: {
      card: 'summary_large_image',
      title: post.title,
      description: post.excerpt,
      creator: '@myhandle',
      images: [post.coverImage],
    },
    
    alternates: {
      canonical: `${baseUrl}/blog/${params.slug}`,
    },
  }
}

export default async function BlogPost({
  params,
}: {
  params: { slug: string }
}) {
  const post = await getPost(params.slug)
  
  return (
    <article>
      <h1>{post.title}</h1>
      <div dangerouslySetInnerHTML={{ __html: post.content }} />
    </article>
  )
}

电商产品页面

// app/products/[id]/page.tsx
export async function generateMetadata({ params }): Promise<Metadata> {
  const product = await getProduct(params.id)
  
  return {
    title: `${product.name} - ${product.price}元`,
    description: product.description,
    
    openGraph: {
      type: 'website',
      title: product.name,
      description: product.description,
      images: product.images.map(img => ({
        url: img.url,
        width: 800,
        height: 600,
        alt: product.name,
      })),
    },
    
    twitter: {
      card: 'summary_large_image',
      title: product.name,
      description: product.description,
      images: [product.images[0].url],
    },
    
    other: {
      'product:price:amount': product.price.toString(),
      'product:price:currency': 'CNY',
    },
  }
}

📚 最佳实践

1. 使用模板减少重复

// app/layout.tsx
export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  title: {
    template: '%s | 我的网站',
    default: '我的网站 - 欢迎页',
  },
  description: '默认描述',
  openGraph: {
    siteName: '我的网站',
    locale: 'zh_CN',
    type: 'website',
  },
}

2. 优化图片尺寸

  • Open Graph: 1200x630px
  • Twitter Card: 1200x675px
  • Favicon: 32x32px, 16x16px
  • Apple Touch Icon: 180x180px

3. 描述长度建议

  • Title: 50-60 字符
  • Description: 150-160 字符
  • Keywords: 5-10 个关键词

4. 使用 JSON-LD 结构化数据

export default function Page() {
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: '文章标题',
    author: {
      '@type': 'Person',
      name: '张三',
    },
    datePublished: '2024-01-01',
  }

  return (
    <>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      <article>...</article>
    </>
  )
}

🔍 测试和验证

测试工具

  1. Facebook Sharing Debugger: https://developers.facebook.com/tools/debug/
  2. Twitter Card Validator: https://cards-dev.twitter.com/validator
  3. Google Rich Results Test: https://search.google.com/test/rich-results
  4. LinkedIn Post Inspector: https://www.linkedin.com/post-inspector/

本地测试

// 查看生成的 HTML
export default function Page() {
  return (
    <head>
      {/* 元数据会自动注入到这里 */}
    </head>
  )
}

🔗 相关资源


下一步:学习 Next.js 错误处理,了解如何优雅地处理应用错误。