Next.js

元数据与 OG 图片

深入理解 Next.js 元数据和 OG 图片优化,包括静态元数据、动态元数据、Open Graph 图片和文件约定

概述

Metadata API 可用于定义应用程序元数据,以改善 SEO 和 Web 可分享性,包括:

  1. 静态 metadata 对象
  2. 动态 generateMetadata 函数
  3. 特殊文件约定,用于添加静态或动态生成的 favicon 和 OG 图片

Next.js 会自动为页面生成相关的 <head> 标签。

metadata 对象和 generateMetadata 函数导出仅在 Server Components 中支持。

默认字段

即使路由未定义元数据,也始终添加两个默认 meta 标签:

<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />

静态元数据

layout.jspage.js 文件导出 Metadata 对象来定义静态元数据。

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'My App',
  description: 'My App Description',
}

export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  )
}

基本字段

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'Home',
  description: 'Welcome to my website',
  keywords: ['Next.js', 'React', 'JavaScript'],
}

export default function Page() {
  return <h1>Home Page</h1>
}

标题模板

使用 title.template 为子页面定义标题模板:

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    template: '%s | My App',
    default: 'My App',
  },
}
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'About',
}

// 输出: <title>About | My App</title>

绝对标题

使用 title.absolute 忽略父布局中的 title.template

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    absolute: 'About',
  },
}

// 输出: <title>About</title>

动态元数据

使用 generateMetadata 函数根据动态信息生成元数据。

import type { Metadata } from 'next'

type Props = {
  params: Promise<{ id: string }>
}

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

  return {
    title: product.title,
    description: product.description,
  }
}

export default async function Page({ params }: Props) {
  const { id } = await params
  return <div>Product {id}</div>
}

使用父元数据

import type { Metadata, ResolvingMetadata } from 'next'

type Props = {
  params: Promise<{ id: string }>
}

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

  // 访问并扩展父元数据
  const previousImages = (await parent).openGraph?.images || []

  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}

元数据字段

title

export const metadata: Metadata = {
  title: 'My Page Title',
}

description

export const metadata: Metadata = {
  description: 'My page description',
}

keywords

export const metadata: Metadata = {
  keywords: ['nextjs', 'react', 'javascript'],
}

authors

export const metadata: Metadata = {
  authors: [{ name: 'John Doe' }, { name: 'Jane Doe', url: 'https://example.com' }],
}

creator

export const metadata: Metadata = {
  creator: 'John Doe',
}

publisher

export const metadata: Metadata = {
  publisher: 'Acme Inc',
}

robots

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

alternates

export const metadata: Metadata = {
  alternates: {
    canonical: 'https://example.com',
    languages: {
      'en-US': 'https://example.com/en-US',
      'zh-CN': 'https://example.com/zh-CN',
    },
  },
}

icons

export const metadata: Metadata = {
  icons: {
    icon: '/icon.png',
    shortcut: '/shortcut-icon.png',
    apple: '/apple-icon.png',
    other: {
      rel: 'apple-touch-icon-precomposed',
      url: '/apple-touch-icon-precomposed.png',
    },
  },
}

openGraph

export const metadata: Metadata = {
  openGraph: {
    title: 'My Page Title',
    description: 'My page description',
    url: 'https://example.com',
    siteName: 'My Site',
    images: [
      {
        url: 'https://example.com/og.png',
        width: 800,
        height: 600,
      },
      {
        url: 'https://example.com/og-alt.png',
        width: 1800,
        height: 1600,
        alt: 'My custom alt',
      },
    ],
    locale: 'en_US',
    type: 'website',
  },
}

twitter

export const metadata: Metadata = {
  twitter: {
    card: 'summary_large_image',
    title: 'My Page Title',
    description: 'My page description',
    siteId: '1467726470533754880',
    creator: '@nextjs',
    creatorId: '1467726470533754880',
    images: ['https://example.com/twitter-image.png'],
  },
}

verification

export const metadata: Metadata = {
  verification: {
    google: 'google-site-verification-code',
    yandex: 'yandex-verification-code',
    yahoo: 'yahoo-verification-code',
    other: {
      me: ['my-email', 'my-link'],
    },
  },
}

Open Graph 图片

静态 OG 图片

在路由段中添加 opengraph-image.(jpg|jpeg|png|gif) 文件:

app/
├── opengraph-image.png
└── about/
    └── opengraph-image.png

Next.js 会自动生成相应的 <meta> 标签。

动态 OG 图片

使用 opengraph-image.tsxopengraph-image.jsx 文件:

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export const alt = 'About Acme'
export const size = {
  width: 1200,
  height: 630,
}
export const contentType = 'image/png'

export default async function Image() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        About Acme
      </div>
    ),
    {
      ...size,
    }
  )
}

使用外部数据

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export const alt = 'Post'
export const size = {
  width: 1200,
  height: 630,
}
export const contentType = 'image/png'

export default async function Image({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params
  const post = await fetch(`https://api.example.com/posts/${slug}`).then((res) =>
    res.json()
  )

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 48,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        <h1>{post.title}</h1>
        <p>{post.description}</p>
      </div>
    ),
    {
      ...size,
    }
  )
}

使用自定义字体

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export default async function Image() {
  const interSemiBold = fetch(
    new URL('./Inter-SemiBold.ttf', import.meta.url)
  ).then((res) => res.arrayBuffer())

  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          fontFamily: 'Inter',
        }}
      >
        Hello world!
      </div>
    ),
    {
      width: 1200,
      height: 630,
      fonts: [
        {
          name: 'Inter',
          data: await interSemiBold,
          style: 'normal',
          weight: 600,
        },
      ],
    }
  )
}

Twitter 图片

静态 Twitter 图片

在路由段中添加 twitter-image.(jpg|jpeg|png|gif) 文件:

app/
├── twitter-image.png
└── about/
    └── twitter-image.png

动态 Twitter 图片

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export const alt = 'About Acme'
export const size = {
  width: 1200,
  height: 630,
}
export const contentType = 'image/png'

export default async function Image() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 128,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        About Acme
      </div>
    ),
    {
      ...size,
    }
  )
}

Favicons

静态 Favicon

app 目录中添加 favicon.ico 文件:

app/
└── favicon.ico

动态 Favicon

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export const size = {
  width: 32,
  height: 32,
}
export const contentType = 'image/png'

export default function Icon() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 24,
          background: 'black',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'white',
        }}
      >
        A
      </div>
    ),
    {
      ...size,
    }
  )
}

Apple Icon

import { ImageResponse } from 'next/og'

export const runtime = 'edge'

export const size = {
  width: 180,
  height: 180,
}
export const contentType = 'image/png'

export default function Icon() {
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 48,
          background: 'black',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'white',
          borderRadius: '20%',
        }}
      >
        A
      </div>
    ),
    {
      ...size,
    }
  )
}

Sitemap

静态 Sitemap

<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
  <url>
    <loc>https://acme.com</loc>
    <lastmod>2023-04-06T15:02:24.021Z</lastmod>
    <changefreq>yearly</changefreq>
    <priority>1</priority>
  </url>
  <url>
    <loc>https://acme.com/about</loc>
    <lastmod>2023-04-06T15:02:24.021Z</lastmod>
    <changefreq>monthly</changefreq>
    <priority>0.8</priority>
  </url>
</urlset>

动态 Sitemap

import { MetadataRoute } from 'next'

export default function sitemap(): MetadataRoute.Sitemap {
  return [
    {
      url: 'https://acme.com',
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 1,
    },
    {
      url: 'https://acme.com/about',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    {
      url: 'https://acme.com/blog',
      lastModified: new Date(),
      changeFrequency: 'weekly',
      priority: 0.5,
    },
  ]
}

从 API 生成 Sitemap

import { MetadataRoute } from 'next'

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
  const posts = await fetch('https://api.example.com/posts').then((res) =>
    res.json()
  )

  const postEntries: MetadataRoute.Sitemap = posts.map((post) => ({
    url: `https://acme.com/blog/${post.slug}`,
    lastModified: new Date(post.updatedAt),
    changeFrequency: 'weekly',
    priority: 0.5,
  }))

  return [
    {
      url: 'https://acme.com',
      lastModified: new Date(),
      changeFrequency: 'yearly',
      priority: 1,
    },
    {
      url: 'https://acme.com/about',
      lastModified: new Date(),
      changeFrequency: 'monthly',
      priority: 0.8,
    },
    ...postEntries,
  ]
}

Robots.txt

静态 Robots

User-Agent: *
Allow: /
Disallow: /private/

Sitemap: https://acme.com/sitemap.xml

动态 Robots

import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: {
      userAgent: '*',
      allow: '/',
      disallow: '/private/',
    },
    sitemap: 'https://acme.com/sitemap.xml',
  }
}

多个用户代理

import { MetadataRoute } from 'next'

export default function robots(): MetadataRoute.Robots {
  return {
    rules: [
      {
        userAgent: 'Googlebot',
        allow: ['/'],
        disallow: '/private/',
      },
      {
        userAgent: ['Applebot', 'Bingbot'],
        disallow: ['/'],
      },
    ],
    sitemap: 'https://acme.com/sitemap.xml',
  }
}

元数据继承和合并

替换字段

export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme is a...',
  },
}
export const metadata = {
  title: 'About',
  openGraph: {
    title: 'About',
  },
}

// 输出:
// <title>About</title>
// <meta property="og:title" content="About" />

继承字段

export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme is a...',
  },
}
export const metadata = {
  title: 'About',
}

// 输出:
// <title>About</title>
// <meta property="og:title" content="Acme" />
// <meta property="og:description" content="Acme is a..." />

最佳实践

1. 在根布局中定义默认元数据

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    template: '%s | My App',
    default: 'My App',
  },
  description: 'My App Description',
  openGraph: {
    siteName: 'My App',
    locale: 'en_US',
    type: 'website',
  },
}

2. 为每个页面定义特定元数据

import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'About',
  description: 'Learn more about us',
}

3. 使用动态元数据获取数据

export async function generateMetadata({ params }): Promise<Metadata> {
  const { slug } = await params
  const post = await getPost(slug)

  return {
    title: post.title,
    description: post.excerpt,
    openGraph: {
      images: [post.coverImage],
    },
  }
}

4. 为 OG 图片使用推荐尺寸

  • Open Graph: 1200x630
  • Twitter Card: 1200x630
  • Favicon: 32x32
  • Apple Icon: 180x180

5. 提供 Sitemap 和 Robots.txt

export default async function sitemap() {
  const posts = await getAllPosts()
  
  return [
    { url: 'https://example.com', priority: 1 },
    ...posts.map((post) => ({
      url: `https://example.com/blog/${post.slug}`,
      lastModified: post.updatedAt,
      priority: 0.8,
    })),
  ]
}

6. 使用验证标签

export const metadata: Metadata = {
  verification: {
    google: 'your-google-verification-code',
  },
}

总结

Next.js Metadata 和 OG Images 提供了强大的 SEO 优化功能:

  1. 静态元数据:使用 metadata 对象定义静态元数据
  2. 动态元数据:使用 generateMetadata 函数根据动态数据生成元数据
  3. Open Graph 图片:支持静态和动态 OG 图片生成
  4. 文件约定:使用特殊文件名自动生成 favicon、OG 图片等
  5. Sitemap:自动或动态生成 sitemap.xml
  6. Robots.txt:配置搜索引擎爬虫规则
  7. 元数据继承:子路由自动继承父路由的元数据
  8. ImageResponse:使用 JSX 和 CSS 动态生成图片

通过合理使用这些功能,可以显著提升应用的 SEO 和社交媒体分享体验。

在 GitHub 上编辑

上次更新于