首页 / Next.js 16 入门教程 / 元数据与SEO

Next.js 16 入门教程

元数据与SEO

本教程共 42 篇 · 第 19 篇 · 更新于 2026-07-30 · 约 8 分钟阅读

Next.jsNext.js 16 入门教程SEO元数据Open GraphgenerateMetadata

19. 元数据与SEO

本节目标:学会使用 Next.js Metadata API 为页面添加标题、描述等 SEO 信息,掌握静态与动态元数据配置,以及 Open Graph 图片的生成方式。

元数据基础

元数据是 HTML <head> 标签中的信息,不会直接显示在页面上,但对搜索引擎和社交平台至关重要。Next.js 提供两种定义元数据的方式:静态 metadata 对象动态 generateMetadata 函数

仅限服务端组件

metadata 对象和 generateMetadata 函数只能在服务端组件中使用。

无论你选择哪种方式,Next.js 都会自动生成相应的 <head> 标签。

默认元数据

即你没有定义任何元数据,Next.js 也会自动添加两个默认标签:

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

静态元数据

layout.jspage.js 文件中导出一个 Metadata 对象:

// app/blog/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: '我的博客',
  description: '分享前端技术与 Next.js 实战经验',
}

export default function Layout({ children }) {
  return <div>{children}</div>
}

也可以使用相对简短的写法(JavaScript 项目中不需要类型注解):

// app/blog/layout.js
export const metadata = {
  title: '我的博客',
  description: '分享前端技术与 Next.js 实战经验',
}

export default function Layout({ children }) {
  return <div>{children}</div>
}

动态元数据

当页面信息依赖外部数据(如数据库查询结果)时,使用 generateMetadata 函数:

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

type Props = {
  params: Promise<{ slug: string }>
  searchParams: Promise<{ [key: string]: string | string[] | undefined }>
}

export async function generateMetadata(
  { params }: Props,
  parent: ResolvingMetadata
): Promise<Metadata> {
  const { slug } = await params

  // 获取文章信息
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(
    (res) => res.json()
  )

  // 可选:继承父级元数据中的图片
  const previousImages = (await parent).openGraph?.images || []

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

export default async function Page({ params }: Props) {
  // 页面内容...
}

Next.js 16 写法 vs 旧版写法

在 Next.js 15+ 中,paramssearchParams 变成了 Promise,需要使用 await 获取值。旧版本(Next.js 14 及之前)中它们是直接的对象。

标题模板

通过 title.template 可以为子路由的标题添加统一前缀或后缀:

// app/layout.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: {
    template: '%s | 码上学',
    default: '码上学', // 使用 template 时必须提供 default
  },
}
// app/blog/page.tsx
export const metadata: Metadata = {
  title: '博客',
}
// 输出: <title>博客 | 码上学</title>

%s 会被子路由的标题替换。title.template 只对子路由生效,不会影响定义它自身的页面。

如果某个页面想要完全忽略父级的模板,使用 title.absolute

export const metadata: Metadata = {
  title: {
    absolute: '关于我们', // 输出: <title>关于我们</title>
  },
}

常用元数据字段

export const metadata: Metadata = {
  // 基础信息
  title: '页面标题',
  description: '页面描述,建议 150 字以内',
  keywords: ['Next.js', 'React', '前端开发'],

  // 作者与版权
  authors: [{ name: '码上学', url: 'https://example.com' }],
  creator: '码上学',
  publisher: '码上学',

  // 搜索引擎控制
  robots: {
    index: true,
    follow: true,
  },

  // 备用链接
  alternates: {
    canonical: 'https://example.com/blog',
    languages: {
      'zh-CN': 'https://example.com/zh-CN',
      'en-US': 'https://example.com/en-US',
    },
  },

  // Open Graph(社交分享)
  openGraph: {
    title: '页面标题',
    description: '页面描述',
    url: 'https://example.com/blog',
    siteName: '码上学',
    images: [
      {
        url: '/og-image.png',
        width: 1200,
        height: 630,
      },
    ],
    locale: 'zh_CN',
    type: 'website',
  },

  // Twitter 卡片
  twitter: {
    card: 'summary_large_image',
    title: '页面标题',
    description: '页面描述',
    images: ['/og-image.png'],
  },
}

metadataBase 配置

metadataBase 是一个便捷选项,用于设置 URL 的基础路径。设置后,子路由中的相对路径会自动拼接为绝对 URL:

export const metadata: Metadata = {
  metadataBase: new URL('https://example.com'),
  alternates: {
    canonical: '/blog', // 自动解析为 https://example.com/blog
  },
  openGraph: {
    images: '/og-image.png', // 自动解析为 https://example.com/og-image.png
  },
}

通常在根布局中设置一次

metadataBase 一般在根 app/layout.tsx 中设置,所有子路由都会继承。

流式元数据

Next.js 15.2+ 支持元数据流式传输。对于动态渲染的页面,Next.js 会先发送 UI 内容,等 generateMetadata 解析完成后再注入元数据标签。这样可以提升首屏渲染速度。

对于无法执行 JavaScript 的爬虫(如 Facebook 的爬虫),Next.js 会自动切换为阻塞模式,确保元数据在 <head> 中可用。你也可以通过 htmlLimitedBots 配置完全禁用流式元数据:

// next.config.ts
import type { NextConfig } from 'next'

const nextConfig: NextConfig = {
  htmlLimitedBots: /.*/,
}

export default nextConfig

避免重复请求

如果同一个数据请求在 generateMetadata 和页面组件中都会用到,使用 React 的 cache 函数避免重复请求:

// app/lib/data.ts
import { cache } from 'react'

export const getPost = cache(async (slug: string) => {
  const res = await db.query.posts.findFirst({ where: eq(posts.slug, slug) })
  return res
})
// app/blog/[slug]/page.tsx
import { getPost } from '@/app/lib/data'

export async function generateMetadata({ params }) {
  const { slug } = await params
  const post = await getPost(slug)
  return { title: post.title }
}

export default async function Page({ params }) {
  const { slug } = await params
  const post = await getPost(slug) // 不会重复请求
  return <article>{post.content}</article>
}

Open Graph 图片生成

Next.js 提供 ImageResponse 构造函数,可以用 JSX 和 CSS 动态生成 OG 图片:

// app/blog/[slug]/opengraph-image.tsx
import { ImageResponse } from 'next/og'

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 getPost(slug)

  return new ImageResponse(
    (
      <div
        style={{
          display: 'flex',
          flexDirection: 'column',
          alignItems: 'center',
          justifyContent: 'center',
          width: '100%',
          height: '100%',
          backgroundColor: '#1a1a2e',
          color: 'white',
          padding: '40px',
        }}
      >
        <h1 style={{ fontSize: 64, textAlign: 'center' }}>{post.title}</h1>
        <p style={{ fontSize: 28, opacity: 0.8 }}>by 码上学</p>
      </div>
    )
  )
}

ImageResponse 的 CSS 限制

ImageResponse 支持 flexbox 和绝对定位,但不支持 display: grid。只支持 CSS 属性的子集,布局需保持简单。

元数据的合并与覆盖规则

元数据从根布局向叶子页面逐级评估并浅合并

  1. app/layout.tsxapp/blog/layout.tsxapp/blog/[slug]/page.tsx
  2. 后定义的字段覆盖先定义的字段
  3. 嵌套字段(如 openGraph)是整体替换,不是深度合并

如果想在不同路由间共享部分元数据同时覆盖其他字段:

// app/shared-metadata.js
export const openGraphImage = { images: ['/default-og.png'] }
// app/page.js
import { openGraphImage } from './shared-metadata'

export const metadata = {
  openGraph: {
    ...openGraphImage,
    title: '首页',
  },
}
// app/about/page.js
import { openGraphImage } from '../shared-metadata'

export const metadata = {
  openGraph: {
    ...openGraphImage,
    title: '关于',
  },
}

小结

  1. 静态元数据:导出 Metadata 对象,适合固定内容
  2. 动态元数据:导出 generateMetadata 函数,适合依赖数据的场景
  3. 标题模板title.template 设置子路由标题的统一格式
  4. 流式传输:Next.js 15.2+ 默认支持,提升首屏速度
  5. OG 图片ImageResponse 用 JSX 生成动态分享图片
  6. 浅合并规则:子路由覆盖父路由,嵌套字段整体替换