JackyLove 的技术人生

人生艰难,唯有一技傍身才能慢慢走向通途。个人博客,记录生活,分享技术,记录成长。专注全栈技术,next技术。

第21章—Metadata篇基于配置

首次发表于 2024-03-22, 更新于 2024-03-22

前言

Metadata,中文译为“元数据”,简单的来说就是描述数据的数据。

例如,一个 HTML 文件是一种数据,但 HTML 文件也能在 <head> 元素中包含描述该文档的数据,比如该文件的标题和字符编码。这些数据就被称为“元数据”。

<head>
  <meta charset="utf-8" />
  <title>我的测试页面</title>
</head>

不止标题和字符编码,元数据是一个非常丰富的概念,<head> 中的各种 meta 标签都是元数据,比如这是描述文档作者和文档信息的元数据:

<meta name="author" content="Chris Mills" />
<meta name="description" content="the description content" />

这是根据 Open Graph 协议编写的元数据,当分享到诸如 Facebook 这样的社交平台的时候,会展示特殊的格式和信息:

<meta property="og:image" content="..." />
<meta property="og:description" content="..." />
<meta property="og:title" content="..." />

网站的自定义图标、robots.txtsitemap.xml 等也是元数据:

<link rel="icon" href="favicon.ico" type="image/x-icon" />

元数据的丰富会改善 SEO 和 web 可共享性(比如定义一些特殊的字段用于内容分享到一些社交平台),为了方便定义和管理这些元数据,Next.js 提供了 Metadata API。在 Next.js 中,添加元数据的方法分为两类:

  1. 基于配置的元数据:在 layout.jspage.js中导出一个静态 metadata 对象或者一个动态的 generateMetadata 函数。
  2. 基于文件的元数据:添加一个静态或者动态生成的特殊文件

通过这些选项,Next.js 会自动为页面生成相关的 <head> 元素。

1. 基于配置(Config-based)

1.1. 静态元数据

要定义一个静态元数据,需要从 layout.js 或者 page.js 文件中导出一个 Metadata 对象:

// layout.js | page.js
export const metadata = {
  title: '...',
  description: '...',
}
 
export default function Page() {}

具体 Metadata 对象有哪些字段呢?我数了数源码中的字段,有 31 个,因为内容很多,所以放在本篇后面具体讲解。我们先了解下主要的定义元数据的方式。

1.2. 动态元数据

动态元数据是指依赖动态信息如当前路由参数、外部数据、父级路由段metadata等信息的元数据。要定义动态元数据,需要导出一个名为 generateMetadata的函数,该函数返回一个 Metadata 对象。使用示例如下:

// app/products/[id]/page.js
export async function generateMetadata({ params, searchParams }, parent) {
  // 读取路由参数
  const id = params.id
 
  // 获取数据
  const product = await fetch(`https://.../${id}`).then((res) => res.json())
 
  // 获取和拓展父路由段 metadata
  const previousImages = (await parent).openGraph?.images || []
 
  return {
    title: product.title,
    openGraph: {
      images: ['/some-specific-page-image.jpg', ...previousImages],
    },
  }
}
 
export default function Page({ params, searchParams }) {}

让我们详细讲解下 generateMetadata 函数。该函数接收两个参数,propsparent

props 指的是包含当前路由参数的对象,该对象又有两个字段,一个是 params,一个是 searchParams

  • params 包含当前动态路由参数的对象,让我们举些例子你就知道了:
Route URL params
app/shop/[slug]/page.js /shop/1 { slug: '1' }
app/shop/[tag]/[item]/page.js /shop/1/2 { tag: '1', item: '2' }
app/shop/[...slug]/page.js /shop/1/2 { slug: ['1', '2'] }

那如果访问的不是动态路由呢?那 params 就是一个空对象 {}

  • searchParams 是一个包含当前 URL 搜索参数的对象,举个例子:
URL 网址 searchParams
/shop?a=1 { a: '1' }
/shop?a=1&b=2 { a: '1', b: '2' }
/shop?a=1&a=2 { a: ['1', '2'] }

searchParams 只在 page.js 中会有。

parent 是一个包含父路由段 metadata 对象的 promise 对象,所以示例代码中用的是 (await parent).openGraph获取。

注意:无论是静态元数据还是动态元数据都只在服务端组件中支持。如果不依赖运行时的信息,则应该尽可能使用静态元数据方式。

缓存篇里也会讲,在 generateMetadatagenerateStaticParams、布局、页面和服务端组件中的 fetch 请求会自动记忆化,所以不用担心多次请求有性能损失。

Next.js 会在等待 generateMetadata中数据请求完毕再开始 UI 流式传输给客户端,这可以保证流式响应的第一部分包含 <head> 标签。

2. 基于文件(File-based)

这些特殊的文件都可用于元数据:

  • favicon.ico、apple-icon.jpg 和 icon.jpg
  • opengraph-image.jpg 和 twitter-image.jpg
  • robots.txt
  • sitemap.xml

基于文件是什么意思呢?我们以 icon 为例,icon 被用于设置浏览器网页标签的图标,虽然也可以使用静态元数据的方式设置(具体讲 metadata 对象的时候会讲到),但是 Next.js 提供了更便捷的方式,那就是约定一个文件名为 icon 的图片,格式支持 .ico.jpg, .jpeg, .png, .svg。也就是添加一个名为 icon.(ico|jpg|jpeg|png|svg)的图片,Next.js 就会为该路由生成对应的元数据标签:

<link
  rel="icon"
  href="/icon?<generated>"
  type="image/<generated>"
  sizes="<generated>"
/>

甚至如果你想动态生成该图片也可以实现,约定一个名为 icon.(js|.ts|.tsx)的文件,返回一张图片即可。

其他文件也是同理,使用这种方式快捷直观。不过具体这些文件该怎么设置,因为涉及的 API 太多,统一放在后面讲解。

要注意:基于文件的元数据具有更高的优先级,并会覆盖任何基于配置的元数据。

3. 行为

3.1. 默认字段

有两个默认的 meta 标签,即使路由未定义元数据也会添加,一个是设置网站字符编码的 charset,一个是设置网站视口宽度和比例的 viewport

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

当然这两个元数据标签也都是可以覆盖的。

3.2. 顺序

一个页面和它的布局以及它的父布局,都设置了一些元数据字段,有的字段重复,有的字段不同,那么 Next.js 该怎么处理呢?

这个逻辑其实我们也很容易想到,具体页面的设置应该优先级更高。Next.js 就是按照从根路由到 page.js的顺序处理,比如当你访问 /blog/1,处理顺序为:

  1. app/layout.tsx(根布局)
  2. app/blog/layout.tsx(嵌套布局布局)
  3. app/blog/[slug]/page.tsx(具体页面)

3.3. 字段覆盖

Next.js 会按照这个顺序,将各层元数据对象浅合并,计算得到最终的元数据。重复的将会根据顺序进行替换。让我们举个例子,加入根布局的 metadata 设置为:

// app/layout.js
export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme is a...',
  },
}

page.js 的设置为:

// app/blog/page.js
export const metadata = {
  title: 'Blog',
  openGraph: {
    title: 'Blog',
  },
}
 
// 输出:
// <title>Blog</title>
// <meta property="og:title" content="Blog" />

在这个例子中:

  1. app/layout.js 中的 titleapp/blog/page.js 中的 title 替换
  2. app/layout.js 中的 openGraph 字段被 app/blog/page.js 中的 openGraph 替换,注意因为是浅合并,所以 description 字段丢失了。

如果你想在多个路由之间共享一些字段,那你可以将他们提取到一个单独的变量中,比如在 app下建立一个 shared-metadata.js文件共享的字段:

// app/shared-metadata.js
export const openGraphImage = { images: ['http://...'] }

当需要使用时:

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

在这个例子中,OG 图片在 app/layout.jsapp/about/page.js 中共享,然而 title 却是不同的。

3.4. 字段继承

比如根布局的设置为:

// app/layout.js
export const metadata = {
  title: 'Acme',
  openGraph: {
    title: 'Acme',
    description: 'Acme is a...',
  },
}

页面的设置为:

// app/about/page.js
export const metadata = {
  title: 'About',
}
 
// 输出:
// <title>About</title>
// <meta property="og:title" content="Acme" />
// <meta property="og:description" content="Acme is a..." />

在这个例子中:

  • app/layout.js 中的 titleapp/about/page.js 中的 title 替代
  • app/layout.js 中的所有 openGraph 字段被 app/about/page.js 继承,因为 app/about/page.js 没有设置 openGraph 字段

4. JSON-LD

4.1 介绍

JSON-LD,英文全称:JavaScript Object Notation for Linked Data,当然这个太“全”了,稍微简写一点就是 JSON for Linking Data,官方文档地址:json-ld.org/,是一种基于 JSON 的数据格式,用于向 Google 和其他搜索引擎描述网站上的内容。

比如如果我们在 Google 搜索 「Chocolate in a mug」,我们会看到这样的搜索结果:

这个搜索结果有评分(4.5)、评价(581条)等内容,我们打开页面,就可以看到搜索展示的这个内容其实对应了 application/ld+json 中的内容:

4.2 正常添加

如果我们也要实现这样的效果,方便搜索引擎展现,该怎么做呢?

在页面加入这种结构化数据的方法很简单,只用在页面添加这样一段脚本就可以了:

<script type="application/ld+json">
  // ...
</script>

具体里面的内容需要参考比如 Google 搜索中心提供的《结构化数据常规指南》,如果你写的是文章,参考 Article 章节后,可以写入以下这些属性:

<script type="application/ld+json">
  {
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "这里填写标题",
  "image": [
  "https://ts.yayujs.com/icon-144x144.png"
  ],
  "datePublished": "2021-11-10T22:06:06.000Z",
  "dateModified": "2022-03-04T16:00:00.000Z",
  "author": [{
  "@type": "Person",
  "name": "冴羽",
  "url": "https://github.com/mqyqingfeng/Blog"
  }]
  }
</script>

4.3 Next.js 添加

如何在 Next.js 中添加 JOSN-LD 呢?Next.js 推荐在 layout.jspage.js组件中使用 <script> 标签,使用示例如下:

// app/products/[id]/page.js
export default async function Page({ params }) {
  const product = await getProduct(params.id)
 
  const jsonLd = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name: product.name,
    image: product.image,
    description: product.description,
  }
 
  return (
    <section>
      <script
        type="application/ld+json"
        dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
      />
      {/* ... */}
    </section>
  )
}

如果你使用 TypeScript,可以配合使用 schema-dts 这个包:

import { Product, WithContext } from 'schema-dts'
 
const jsonLd: WithContext<Product> = {
  '@context': 'https://schema.org',
  '@type': 'Product',
  name: 'Next.js Sticker',
  image: 'https://nextjs.org/imgs/sticker.png',
  description: 'Dynamic at the speed of static.',
}

添加 JSON-LD 并部署后,你可以使用 https://search.google.com/test/rich-results 来测试网页是否支持富媒体搜索结果,使用 https://validator.schema.org/ 测试页面的结构化数据,输入 https://yayujs.com试试。

5. Metadata 字段

接下来我们来介绍下 Metadata 对象的字段有哪些,由于字段很多,你可以先了解一下有哪些,需要使用的时候再查找使用。每个字段都配有使用的例子。

5.1. title

设置文档的标题,该值可以是一个简单的字符串,也可以是一个可选的模板对象。

5.1.1. 字符串

先说字符串:

// layout.js | page.js
export const metadata = {
  title: 'Next.js',
}

注意在 layout.jspage.js 中都可以定义,输出结果为:

<title>Next.js</title>

5.1.2. 模板对象

再说模板对象:

// app/layout.js 
export const metadata = {
  title: {
    default: '...',
    template: '...',
    absolute: '...',
  },
}

其中 default为没有定义 title 的子路由提供一个备用 title:

// app/layout.tsx
export const metadata = {
  title: {
    default: 'Acme',
  },
}
// app/about/page.tsx
export const metadata = {}
 
// Output: <title>Acme</title>

template 可用于为子路由的 title 添加一个前缀或者后缀,比如:

// app/layout.js
export const metadata = {
  title: {
    template: '%s | Acme',
    default: 'Acme', // 设置 template 的时候,default 必传
  },
}
// app/about/page.js
export const metadata = {
  title: 'About',
}
 
// Output: <title>About | Acme</title>

使用 template 的时候要注意:

  1. 使用 title.template 的时候,title.default 是必要的
  2. 定义在 layout.js 中的 title.template 不会应用于同级的 page.js 中的 title
  3. 定义在 page.js 中的 title.template 不会有效果,因为页面没有子路由段。
  4. 如果路由没有定义 title 或者 title.defaulttitle.template 是不生效的

absolute用于设置标题,但会忽略父路由段中设置的 title.template

// app/layout.js
export const metadata = {
  title: {
    template: '%s | Acme',
  },
}
// app/about/page.js
export const metadata = {
  title: {
    absolute: 'About',
  },
}
 
// Output: <title>About</title>

这三个值在 layout.jspage.js 中的表现会有不同,让我们细致梳理一下:

layout.js 中:

  1. title(字符串)和 title.default 定义的是未设置标题的子路由段的默认标题。它会根据最近的父路由段中的 title.template 进行拓展。举个例子:
// app/layout.js
export const metadata = {
  title: {
    default: 'home',
    template: '%s | Home'
  }
}
// app/about/layout.js
export const metadata = {
  title: {
    default: 'about'
  }
}

如果 /about/company没有定义自己标题,那它的标题为:about | Home。而如果最近的父路由段没有定义 title.template,那就不进行拓展。

PS:不过根据 demo 测试,如果 app/layout.js 如上相同设置,app/about/layout.js 不设置,app/about/company/layout.js 设置 default: 'company',访问 /about/company/members时,标题为 company | Home,也就是说实际的测试结果是如果完全不设置 metadata,默认标题会根据最近的存在的 title.template 进行扩展,并不一定是最近的父路由段中的 title.template

  1. title.absolute 定义子路由段的默认标题,它会忽略来自父路由段的 title.template,也就是说即使最近的父路由段有 title.template,也不进行拓展(所以才叫 "absoulte" 绝对标题)
  2. title.template 为子路由段定义一个新标题模板,也就是说即使每层 layout 都设置了 title.template,也不会出现 xxx | Company | About | Home 这种标题,设置标题的子路由段只会根据父路由段的 title.template 进行设置。

page.js 中:

  1. 如果一个页面没有定义自己的标题,它会使用最近的父级解析标题,注意是“最近的‘父级解析标题’”,而非“‘最近的父级’解析标题”。

PS:根据 demo 测试,如果 page.js 未设置 title,会优先使用同级 layout.js 中设置的 title,包括字符串、title.default title.abslolute 的方式。

如果祖先 layout.js 中只有一层设置了标题,而其他都未设置,页面的标题会使用该标题,比如 app/layout.js中设置 title: 'home'app/about/company/members中的各层 layout.js都未设置,/about/company/memberstitlehome。欢迎写 demo 验证。

  1. 如果 page.js 定义了路由 title(字符串),它会根据最近的父路由段中的 title.template 进行拓展。

PS:不过根据 demo 测试,如同 layout.js,如果父级完全不设置 metadata,标题会根据最近的存在的 title.template 进行扩展,并不一定是最近的父路由段中的 title.template

换句话说,如果不设置 layout.jsmetadata,在标题的处理上,相当于这层不存在。

  1. 使用 title.absolute 定义路由标题,它会忽略父路由段中的 title.template.
  2. page.js 中设置 title.template没有效果。

总之,在使用的时候,尽可能保证每个页面都有设置 title,就不会遇到奇怪的逻辑了。

5.2. description

设置页面的描述:

// layout.js | page.js
export const metadata = {
  description: 'The React Framework for the Web',
}

输出的 HTML 为:

<meta name="description" content="The React Framework for the Web" />

5.3. 基础字段

// layout.js | page.js
export const metadata = {
  generator: 'Next.js',
  applicationName: 'Next.js',
  referrer: 'origin-when-cross-origin',
  keywords: ['Next.js', 'React', 'JavaScript'],
  authors: [{ name: 'Seb' }, { name: 'Josh', url: 'https://nextjs.org' }],
  creator: 'Jiachi Liu',
  publisher: 'Sebastian Markbåge',
  formatDetection: {
    email: false,
    address: false,
    telephone: false,
  },
}

输出的 HTML 为:

<meta name="application-name" content="Next.js" />
<meta name="author" content="Seb" />
<link rel="author" href="https://nextjs.org" />
<meta name="author" content="Josh" />
<meta name="generator" content="Next.js" />
<meta name="keywords" content="Next.js,React,JavaScript" />
<meta name="referrer" content="origin-when-cross-origin" />
<meta name="color-scheme" content="dark" />
<meta name="creator" content="Jiachi Liu" />
<meta name="publisher" content="Sebastian Markbåge" />
<meta name="format-detection" content="telephone=no, address=no, email=no" />

5.4. metadataBase

设置 metadata 字段中地址的 URL 前缀,看个例子就明白了:

// layout.js | page.js
export const metadata = {
  metadataBase: new URL('https://acme.com'),
  alternates: {
    canonical: '/',
    languages: {
      'en-US': '/en-US',
      'de-DE': '/de-DE',
    },
  },
  openGraph: {
    images: '/og-image.png',
  },
}

输出的 HTML 为:

<link rel="canonical" href="https://acme.com" />
<link rel="alternate" hreflang="en-US" href="https://acme.com/en-US" />
<link rel="alternate" hreflang="de-DE" href="https://acme.com/de-DE" />
<meta property="og:image" content="https://acme.com/og-image.png" />

关于 metadataBasemetadata,Next.js 会自动处理,并不是简单的字符串拼接,比如当你设置 metadataBase为:

// app/layout.js
export const metadata = {
  metadataBase: new URL('https://acme.com'),
}

如果你传入 metadata 的值为:

metadata 字段 解析 URL
/ https://acme.com
./ https://acme.com
payments https://acme.com/payments
/payments https://acme.com/payments
./payments https://acme.com/payments
../payments https://acme.com/payments
https://beta.acme.com/payments https://beta.acme.com/payments

其他关于 metadataBase:

  1. metadataBase 通常设置在app/layout.js,应用于所有路由
  2. metadataBase 可以设置的更具体,比如 https://app.acme.comhttps://acme.com/start/from/here
  3. 如果 metadata 提供了绝对地址,metadataBase 就会被忽略
  4. 在没有配置 metadataBase 的时候,metadata 使用相对地址会报错
  5. Next.js 会在 metadataBase 和相对地址间处理多余的斜线,比如 metadataBasehttps://acme.com/metadata/path,Next.js 也能正确处理为 https://acme.com/path

5.5. openGraph

// layout.js | page.js
export const metadata = {
  openGraph: {
    title: 'Next.js',
    description: 'The React Framework for the Web',
    url: 'https://nextjs.org',
    siteName: 'Next.js',
    images: [
      {
        url: 'https://nextjs.org/og.png',
        width: 800,
        height: 600,
      },
      {
        url: 'https://nextjs.org/og-alt.png',
        width: 1800,
        height: 1600,
        alt: 'My custom alt',
      },
    ],
    locale: 'en_US',
    type: 'website',
  },
}

输出的 HTML 为:

<meta property="og:title" content="Next.js" />
<meta property="og:description" content="The React Framework for the Web" />
<meta property="og:url" content="https://nextjs.org/" />
<meta property="og:site_name" content="Next.js" />
<meta property="og:locale" content="en_US" />
<meta property="og:image:url" content="https://nextjs.org/og.png" />
<meta property="og:image:width" content="800" />
<meta property="og:image:height" content="600" />
<meta property="og:image:url" content="https://nextjs.org/og-alt.png" />
<meta property="og:image:width" content="1800" />
<meta property="og:image:height" content="1600" />
<meta property="og:image:alt" content="My custom alt" />
<meta property="og:type" content="website" />
export const metadata = {
  openGraph: {
    title: 'Next.js',
    description: 'The React Framework for the Web',
    type: 'article',
    publishedTime: '2023-01-01T00:00:00.000Z',
    authors: ['Seb', 'Josh'],
  },
}

输出的 HTML 为:

<meta property="og:title" content="Next.js" />
<meta property="og:description" content="The React Framework for the Web" />
<meta property="og:type" content="article" />
<meta property="article:published_time" content="2023-01-01T00:00:00.000Z" />
<meta property="article:author" content="Seb" />
<meta property="article:author" content="Josh" />

5.6. robots

// layout.js | page.js
export const metadata = {
  robots: {
    index: false,
    follow: true,
    nocache: true,
    googleBot: {
      index: true,
      follow: false,
      noimageindex: true,
      'max-video-preview': -1,
      'max-image-preview': 'large',
      'max-snippet': -1,
    },
  },
}

输出的 HTML 为:

<meta name="robots" content="noindex, follow, nocache" />
<meta
  name="googlebot"
  content="index, nofollow, noimageindex, max-video-preview:-1, max-image-preview:large, max-snippet:-1"
/>

5.7. icons

// layout.js | page.js
export const 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',
    },
  },
}

输出的 HTML 为:

<link rel="shortcut icon" href="/shortcut-icon.png" />
<link rel="icon" href="/icon.png" />
<link rel="apple-touch-icon" href="/apple-icon.png" />
<link
  rel="apple-touch-icon-precomposed"
  href="/apple-touch-icon-precomposed.png"
/>
export const metadata = {
  icons: {
    icon: [{ url: '/icon.png' }, new URL('/icon.png', 'https://example.com')],
    shortcut: ['/shortcut-icon.png'],
    apple: [
      { url: '/apple-icon.png' },
      { url: '/apple-icon-x3.png', sizes: '180x180', type: 'image/png' },
    ],
    other: [
      {
        rel: 'apple-touch-icon-precomposed',
        url: '/apple-touch-icon-precomposed.png',
      },
    ],
  },
}

输出的 HTML 为:

<link rel="shortcut icon" href="/shortcut-icon.png" />
<link rel="icon" href="/icon.png" />
<link rel="apple-touch-icon" href="/apple-icon.png" />
<link
  rel="apple-touch-icon-precomposed"
  href="/apple-touch-icon-precomposed.png"
/>
<link rel="icon" href="https://example.com/icon.png" />
<link
  rel="apple-touch-icon"
  href="/apple-icon-x3.png"
  sizes="180x180"
  type="image/png"
/>

5.8. themeColor

metadata 中的 themeColor 已废弃,使用 viewport 配置项

5.9. manifest

// layout.js | page.js
export const metadata = {
  manifest: 'https://nextjs.org/manifest.json',
}

输出的 HTML 为:

<link rel="manifest" href="https://nextjs.org/manifest.json" />

5.10. twitter

// layout.js | page.js
export const metadata = {
  twitter: {
    card: 'summary_large_image',
    title: 'Next.js',
    description: 'The React Framework for the Web',
    siteId: '1467726470533754880',
    creator: '@nextjs',
    creatorId: '1467726470533754880',
    images: ['https://nextjs.org/og.png'],
  },
}

输出的 HTML 为:

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:site:id" content="1467726470533754880" />
<meta name="twitter:creator" content="@nextjs" />
<meta name="twitter:creator:id" content="1467726470533754880" />
<meta name="twitter:title" content="Next.js" />
<meta name="twitter:description" content="The React Framework for the Web" />
<meta name="twitter:image" content="https://nextjs.org/og.png" />
// layout.js | page.js
export const metadata = {
  twitter: {
    card: 'app',
    title: 'Next.js',
    description: 'The React Framework for the Web',
    siteId: '1467726470533754880',
    creator: '@nextjs',
    creatorId: '1467726470533754880',
    images: {
      url: 'https://nextjs.org/og.png',
      alt: 'Next.js Logo',
    },
    app: {
      name: 'twitter_app',
      id: {
        iphone: 'twitter_app://iphone',
        ipad: 'twitter_app://ipad',
        googleplay: 'twitter_app://googleplay',
      },
      url: {
        iphone: 'https://iphone_url',
        ipad: 'https://ipad_url',
      },
    },
  },
}

输出的 HTML 为:

<meta name="twitter:site:id" content="1467726470533754880" />
<meta name="twitter:creator" content="@nextjs" />
<meta name="twitter:creator:id" content="1467726470533754880" />
<meta name="twitter:title" content="Next.js" />
<meta name="twitter:description" content="The React Framework for the Web" />
<meta name="twitter:card" content="app" />
<meta name="twitter:image" content="https://nextjs.org/og.png" />
<meta name="twitter:image:alt" content="Next.js Logo" />
<meta name="twitter:app:name:iphone" content="twitter_app" />
<meta name="twitter:app:id:iphone" content="twitter_app://iphone" />
<meta name="twitter:app:id:ipad" content="twitter_app://ipad" />
<meta name="twitter:app:id:googleplay" content="twitter_app://googleplay" />
<meta name="twitter:app:url:iphone" content="https://iphone_url" />
<meta name="twitter:app:url:ipad" content="https://ipad_url" />
<meta name="twitter:app:name:ipad" content="twitter_app" />
<meta name="twitter:app:name:googleplay" content="twitter_app" />

5.11. viewport

Next.js 14 起已废弃,使用 viewport 配置项

5.12. verification

// layout.js | page.js
export const metadata = {
  verification: {
    google: 'google',
    yandex: 'yandex',
    yahoo: 'yahoo',
    other: {
      me: ['my-email', 'my-link'],
    },
  },
}

输出的 HTML 为:

<meta name="google-site-verification" content="google" />
<meta name="y_key" content="yahoo" />
<meta name="yandex-verification" content="yandex" />
<meta name="me" content="my-email" />
<meta name="me" content="my-link" />

5.13. appleWebApp

// layout.js | page.js
export const metadata = {
  itunes: {
    appId: 'myAppStoreID',
    appArgument: 'myAppArgument',
  },
  appleWebApp: {
    title: 'Apple Web App',
    statusBarStyle: 'black-translucent',
    startupImage: [
      '/assets/startup/apple-touch-startup-image-768x1004.png',
      {
        url: '/assets/startup/apple-touch-startup-image-1536x2008.png',
        media: '(device-width: 768px) and (device-height: 1024px)',
      },
    ],
  },
}

输出的 HTML 为:

<meta
  name="apple-itunes-app"
  content="app-id=myAppStoreID, app-argument=myAppArgument"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-title" content="Apple Web App" />
<link
  href="/assets/startup/apple-touch-startup-image-768x1004.png"
  rel="apple-touch-startup-image"
/>
<link
  href="/assets/startup/apple-touch-startup-image-1536x2008.png"
  media="(device-width: 768px) and (device-height: 1024px)"
  rel="apple-touch-startup-image"
/>
<meta
  name="apple-mobile-web-app-status-bar-style"
  content="black-translucent"
/>

5.14. alternates

// layout.js | page.js
export const metadata = {
  alternates: {
    canonical: 'https://nextjs.org',
    languages: {
      'en-US': 'https://nextjs.org/en-US',
      'de-DE': 'https://nextjs.org/de-DE',
    },
    media: {
      'only screen and (max-width: 600px)': 'https://nextjs.org/mobile',
    },
    types: {
      'application/rss+xml': 'https://nextjs.org/rss',
    },
  },
}

输出的 HTML 为:

<link rel="canonical" href="https://nextjs.org" />
<link rel="alternate" hreflang="en-US" href="https://nextjs.org/en-US" />
<link rel="alternate" hreflang="de-DE" href="https://nextjs.org/de-DE" />
<link
  rel="alternate"
  media="only screen and (max-width: 600px)"
  href="https://nextjs.org/mobile"
/>
<link
  rel="alternate"
  type="application/rss+xml"
  href="https://nextjs.org/rss"
/>

5.15. appLinks

// layout.js | page.js
export const metadata = {
  appLinks: {
    ios: {
      url: 'https://nextjs.org/ios',
      app_store_id: 'app_store_id',
    },
    android: {
      package: 'com.example.android/package',
      app_name: 'app_name_android',
    },
    web: {
      url: 'https://nextjs.org/web',
      should_fallback: true,
    },
  },
}

输出的 HTML 为:

<meta property="al:ios:url" content="https://nextjs.org/ios" />
<meta property="al:ios:app_store_id" content="app_store_id" />
<meta property="al:android:package" content="com.example.android/package" />
<meta property="al:android:app_name" content="app_name_android" />
<meta property="al:web:url" content="https://nextjs.org/web" />
<meta property="al:web:should_fallback" content="true" />

5.16. archives

// layout.js | page.js
export const metadata = {
  archives: ['https://nextjs.org/13'],
}

输出的 HTML 为:

<link rel="archives" href="https://nextjs.org/13" />

5.17. assets

// layout.js | page.js
export const metadata = {
  assets: ['https://nextjs.org/assets'],
}

输出的 HTML 为:

<link rel="assets" href="https://nextjs.org/assets" />

5.18. bookmarks

// layout.js | page.js
export const metadata = {
  bookmarks: ['https://nextjs.org/13'],
}

输出的 HTML 为:

<link rel="bookmarks" href="https://nextjs.org/13" />

5.19. category

// layout.js | page.js
export const metadata = {
  category: 'technology',
}

输出的 HTML 为:

<meta name="category" content="technology" />

其他

所有的 metadata 选项都可以通过像本节介绍的这种方式直接覆盖,Next.js 内部已经定义了这些字段。然而你也可能会想自定义一些 metadata 标签,此时就可以使用 other 选项:

export const metadata = {
  other: {
    custom: 'meta',
  },
}

输出的 HTML 为:

<meta name="custom" content="meta" />

参考链接

  1. Optimizing: Metadata
  2. Metadata - MDN Web 文档术语表:Web 相关术语的定义 | MDN
  3. “头”里有什么——HTML 元信息 - 学习 Web 开发 | MDN
  4. https://nextjs.org/docs/app/api-reference/functions/generate-metadata
© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru