Next.js Metadata

Overview

Next.js provides a powerful Metadata API for defining application metadata (such as title, description, Open Graph tags, and more), optimizing SEO and social media sharing.

Why Metadata Management?

Importance of Metadata

  1. SEO Optimization: Helps search engines understand page content
  2. Social Sharing: Optimizes how content appears on social media
  3. User Experience: Provides accurate page titles and descriptions
  4. Brand Recognition: Consistent icons and theme colors

Basic Usage

Static Metadata

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

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

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

Dynamic Metadata

// 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>
}

Metadata Fields

Basic Fields

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',
    },
  },
}

Title Templates

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

Open Graph Metadata

Basic 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: '网站预览图',
      },
    ],
  },
}

Article Type

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 Cards

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: '文章预览图',
    },
  },
}

Icons and Theme

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',
    },
  },
}

Multiple Icon Sizes

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' },
    ],
  },
}

Theme Color

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

Robots and Indexing

Robots Configuration

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,
    },
  },
}

Blocking Indexing

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

Mobile Optimization

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',
  },
}

Dynamic Metadata Generation

Fetching Data from an 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>
}

Inheriting Parent Metadata

// 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,
  }
}

Practical Examples

Complete Blog Post Metadata

// 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>
  )
}

E-Commerce Product Page

// 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',
    },
  }
}

Best Practices

1. Use Templates to Reduce Duplication

// 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. Optimize Image Dimensions

  • Open Graph: 1200x630px
  • Twitter Card: 1200x675px
  • Favicon: 32x32px, 16x16px
  • Apple Touch Icon: 180x180px
  • Title: 50–60 characters
  • Description: 150–160 characters
  • Keywords: 5–10 keywords

4. Use JSON-LD Structured Data

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>
    </>
  )
}

Testing and Validation

Testing Tools

  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/

Local Testing

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

Previous Chapter: Font Optimization | Next Chapter: Error Handling