JackyLove 的技术人生

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

第22章—Metadata篇基于文件

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

前言

上篇我们讲到添加元数据的方法分为两类:

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

其中基于文件的元数据可以用来添加网站的应用图标、OG 图片、robots.txt、sitemap.xml、manifest.json。本篇我们详细讲解具体的用法和配置内容。

1. favicon、icon 和 apple-icon

1.1. 介绍

Next.js 约定 faviconiconapple-icon文件都可以用来设置应用的图标。它们可以设置浏览器标签页中的图标、收藏夹(书签)中的图标、手机屏幕图标、搜索引擎结果中的图标等等。

有两种方式可以设置图标:

  1. 使用静态文件
  2. 使用代码生成

让我们一一开始介绍。

1.2. 使用静态文件

1.2.1. 文件约定

/app目录下放置一个名为 faviconicon 或者 apple-icon图片文件即可设置图标。

它们之间的区别在于 favicon 图片只能位于 app/ 根目录(当然这样说不太严谨,考虑到路由组的存在,准确的说应该是顶层)。icon 可以放在更深层的目录里,更精细的设置图标。apple-icon 则顾名思义,设置在苹果设备中显示的图标。这三种文件对应的图片格式、生效目录、和 rel 属性如下:

文件名称 支持的图片格式 生效目录 对应 <link> 标签 rel 属性
favicon .ico app/ <link rel="icon" />
icon .ico.jpg.jpeg.png.svg app/**/* <link rel="icon" />
apple-icon .jpg.jpeg.png app/**/* <link rel="apple-touch-icon" />

1.2.2. favicon

favicon 是 favorites icon 的缩写,用于设置网站或网页相关的图标。最早定义 favicon 的方式是将一个名为 favicon.ico的文件放在服务器根目录下,浏览器会在加载网页的时候,请求 /favicon.ico文件作为图标。后来才出现了使用 <link> 标签这种更为灵活的方法:

<link rel="icon" href="/favicon.ico" />

目前大多数浏览器都支持这两种方法,在 Next.js 中分别对应着使用 favicon 和使用 icon 文件。

/app下添加一个名为 favicon.ico的图片文件,对应 HTML 输出为:

<link rel="icon" href="/favicon.ico" sizes="any" />

注意其中 sizes="any",这是为了避免一个浏览器 bug 而特意这样写的。添加文件后,你可以通过访问 /favicon.ico(对应到本地开发的时候,地址是 http://localhost:3000/favicon.ico )查看该图标文件。该图标默认会应用于网站所有路由,如果想更细粒度的控制某个网页的图标,那就用 icon。

1.2.3. icon

添加一个 icon.(ico|jpg|jpeg|png|svg)图片文件,对应 HTML 输出为:

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

1.2.4. apple-icon

apple-icon,顾名思义,针对苹果设备使用的图标,用于将网页添加到 iPhone 或 iPad 屏幕快捷方式时使用的图标。添加一个 apple-icon.(jpg|jpeg|png)图片文件,对应 HTML 输出为:

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

1.2.5. 行为

你可以通过在文件名中添加数字后缀设置多个图标,比如 icon1.pngicon2.pngapple-icon1.pngapple-icon2.png

为什么会需要多个图标呢?我们在查看页面源码的时候,经常会看到这样的代码:

<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">

根据设备、应用、场景的不同,会使用不同尺寸大小的图片,比如不同的浏览器、不同的场景(标签、Google 搜索结果、书签、移动端图标)、不同的系统如 IOS 和 Windows 使用的图片大小都可能不同。

而生成的 <link> 标签的属性如 relhreftypesizes是根据图标类型和文件内容生成的,比如一个 32x32 大小的 .png图标生成的属性有 type="image/png"sizes="32x32"

你可以使用这两个网站帮助生成 icon:

  1. https://realfavicongenerator.net/
  2. https://www.favicon.cc/

1.3. 使用代码生成

1.3.1. 介绍

除了直接使用图片文件,你也可以用代码生成图标。依然是新建一个名为 iconapple-icon的文件,不过这次的后缀是 .js.ts.tsx

最简单的生成一个图标的方式是通过 next/ogImageResponse API,使用示例如下:

// app/icon.js
import { ImageResponse } from 'next/og'
 
// 路由段配置
export const runtime = 'edge'
 
// 图片 metadata
export const size = {
  width: 32,
  height: 32,
}
export const contentType = 'image/png'
 
// 图片生成
export default function Icon() {
  return new ImageResponse(
    (
      // ImageResponse JSX 元素
      <div
        style={{
          fontSize: 24,
          background: 'black',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          color: 'white',
        }}
      >
        A
      </div>
    ),
    // ImageResponse options
    {
      // 方便复用 size
      ...size,
    }
  )
}

对应输出的 HTML 为:

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

效果为:

image.png

第一次看这个例子的时候,可能会有很多疑问,ImageResponse是什么?怎么用?其中的写法怎么这么奇怪?配置项有哪些等等。不用着急,我们会一一讲解。

先说几点注意事项:

  1. 从 v14.0.0 起,ImageResponsenext/server转移到 next/og,如果出现导入错误,那就升级版本或者使用 next/server
  2. 默认情况下,生成的图标是静态优化的,也就是说会在构建的时候生成并缓存,除非使用了动态函数(比如 cookies()、headers() 这些)或者未缓存数据。
  3. 你不能生成 favicon 图标。使用 icon 或者 favicon.ico 静态文件代替。
  4. 你可以使用 generateImageMetadata API 在一个文件中生成多个 Icon。(好家伙,又多了一个要介绍的 API)

接下来我们详细介绍下涉及到的 API。

1.3.2. 默认导出函数

默认导出函数接收一个可选参数 params,这是一个包含动态路由参数的对象,与上一篇介绍 generateMetadata 中的 params 参数一样:

// app/shop/[slug]/icon.js
export default function Image({ params }) {
  // ...
}
Route URL params
app/shop/icon.js /shop {}
app/shop/[slug]/icon.js /shop/1 { slug: '1' }
app/shop/[tag]/[item]/icon.js /shop/1/2 { tag: '1', item: '2' }
app/shop/[...slug]/icon.js /shop/1/2 { slug: ['1', '2'] }

函数应该返回一个 Blob | ArrayBuffer | TypedArray | DataView | ReadableStream | Response 类型的值。

1.3.3. 图片元数据配置

除了导出图片文件本身,你也可以选择性的导出sizecontentType 变量来设置该图片的元数据:

Option Type
size { width: number; height: number }
contentType string (具体有哪些值,参考 image MIME type

设置 size

// icon.js | apple-icon.js
export const size = { width: 32, height: 32 }
 
export default function Icon() {}

对应 HTML 输出为:

<link rel="icon" sizes="32x32" />

设置 contentType

// icon.js | apple-icon.js
export const contentType = 'image/png'
 
export default function Icon() {}

对应 HTML 输出为:

<link rel="icon" type="image/png" />

1.3.4. 路由段配置

iconapple-icon 其实是特殊的路由处理程序,所以它们也可以像其他页面和布局一样,使用路由段配置:

Option Type 默认值
dynamic 'auto' &#124; 'force-dynamic' &#124; 'error' &#124; 'force-static' 'auto'
revalidate false &#124; 'force-cache' &#124; 0 &#124; number false
runtime 'nodejs' &#124; 'edge' 'nodejs'
preferredRegion 'auto' &#124; 'global' &#124; 'home' &#124; string &#124; string[] 'auto'

使用示例如下:

// app/icon.js
export const runtime = 'edge'
 
export default function Icon() {}

1.4. ImageResponse

关于 ImageResponse 构造函数,它可以帮助你使用 JSX 和 CSS 生成动态图片,这对于生成社交媒体图片(Open Graph 图像、Twitter 卡片等)非常有用,因为这些图片往往依赖于动态的内容。

ImageResponse 的 Type 为:

import { ImageResponse } from 'next/og'
 
new ImageResponse(
  element: ReactElement,
  options: {
    width?: number = 1200
    height?: number = 630
    emoji?: 'twemoji' | 'blobmoji' | 'noto' | 'openmoji' = 'twemoji',
    fonts?: {
      name: string,
      data: ArrayBuffer,
      weight: number,
      style: 'normal' | 'italic'
    }[]
    debug?: boolean = false
 
    // Options that will be passed to the HTTP response
    status?: number = 200
    statusText?: string
    headers?: Record<string, string>
  },
)

在具体的实现上,其实用的是 Vercel 自家产品 satori,这是一个支持 JSX,将 HTML 和 CSS 转为 SVG 的库,所以关于 ImageResponse 具体如何写,支持哪些 HTML 和 CSS,参考 satori 的文档即可。

还有一个 generateImageMetadata API ,搭配默认导出函数,可以生成多张图片,下节结束后会讲到。

2. opengraph-image 和 twitter-image

2.1. 介绍

关于 opengraph 的具体作用可以参考我写的《VuePress 博客之 SEO 优化(四) Open Graph protocol》。

简单的来说,按照 Open Graph Protocol 这个协议描述页面信息,社交网站(如 Facebook)就会按照页面上 og 标签的内容呈现给用户。twitter-image 作用类似,只不过是用于 twitter。

使用方式有两种,一种是静态图片文件,一种是使用代码生成。

2.2. 静态文件

与添加图标文件一样,直接在路由文件夹下添加对应的图片即可,只不过约定的文件名和支持的格式不同:

文件名称 支持的图片格式
opengraph-image .jpg.jpeg.png.gif
twitter-image .jpg.jpeg.png.gif
opengraph-image.alt .txt
twitter-image.alt .txt

2.2.1. opengraph-image

添加一个 opengraph-image.(jpg|jpeg|png|gif)图片文件,对应 HTML 输出为:

<meta property="og:image" content="<generated>" />
<meta property="og:image:type" content="<generated>" />
<meta property="og:image:width" content="<generated>" />
<meta property="og:image:height" content="<generated>" />

2.2.2 twitter-image

添加一个 twitter-image.(jpg|jpeg|png|gif)图片文件,对应 HTML 输出为:

<meta name="twitter:image" content="<generated>" />
<meta name="twitter:image:type" content="<generated>" />
<meta name="twitter:image:width" content="<generated>" />
<meta name="twitter:image:height" content="<generated>" />

2.2.3. opengraph-image.alt.txt

当使用 opengraph-image.(jpg|jpeg|png|gif)图片的时候,再添加一个 opengraph-image.alt.txt作为该图片的 alt 文字:

About Acme

对应输出的 HTML 为:

<meta property="og:image:alt" content="About Acme" />

2.2.4. twitter-image.alt.txt

当使用 twitter-image.(jpg|jpeg|png|gif)图片的时候,再添加一个 twitter-image.alt.txt作为该图片的 alt 文字:

About Acme

对应输出的 HTML 为:

<meta property="twitter:image:alt" content="About Acme" />

2.3. 代码生成

2.3.1 介绍

想必你已经知道该怎么做了:

文件名 支持的文件格式
opengraph-image .js.ts.tsx
twitter-image .js.ts.tsx

代码与上节的图标图片生成类似,默认导出函数、路由段配置都相同,图片元数据配置上略有差别。示例代码如下:

// app/about/opengraph-image.js
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() {
  // Font
  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',
        }}
      >
        About Acme
      </div>
    ),
    {
      ...size,
      fonts: [
        {
          name: 'Inter',
          data: await interSemiBold,
          style: 'normal',
          weight: 400,
        },
      ],
    }
  )
}

这个例子顺便演示了如何使用字体文件生成图片,对应输出的 HTML 为:

<meta property="og:image" content="<generated>" />
<meta property="og:image:alt" content="About Acme" />
<meta property="og:image:type" content="image/png" />
<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

2.3.2. 图片元数据配置

除了导出图片文件本身,你也可以选择性的导出 altsizecontentType 变量来设置该图片的元数据:

Option Type
alt string
size { width: number; height: number }
contentType string (具体有哪些值,参考 image MIME type

相比图标,多了一个 alt 变量。设置 alt

// opengraph-image.js | twitter-image.js
export const alt = 'My images alt text'
 
export default function Image() {}

对应 HTML 输出为:

<meta property="og:image:alt" content="My images alt text" />

设置 size

// opengraph-image.js | twitter-image.js
export const size = { width: 1200, height: 630 }
 
export default function Image() {}

对应 HTML 输出为:

<meta property="og:image:width" content="1200" />
<meta property="og:image:height" content="630" />

设置 contentType

export const contentType = 'image/png'
 
export default function Image() {}

对应 HTML 输出为:

<meta property="og:image:type" content="image/png" />

2.3.3. 使用外部数据

图标的生成往往不需要使用外部数据,但是 opengraph-image 和 twitter-image 可能需要,比如当我们访问 /posts/1的时候,可以获取该文章的信息,然后使用文章的标题生成一张图片:

// app/posts/[slug]/opengraph-image.js
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({ params }) {
  const post = await fetch(`https://.../posts/${params.slug}`).then((res) =>
    res.json()
  )
 
  return new ImageResponse(
    (
      <div
        style={{
          fontSize: 48,
          background: 'white',
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
        }}
      >
        {post.title}
      </div>
    ),
    {
      ...size,
    }
  )
}

3. generateImageMetadata 生成多张图片

使用 generateImageMetadata 可以生成一张图片的不同版本或者返回多张图片。使用语法如下:

// icon.js
export function generateImageMetadata({ params }) {
  // ...
}

params 跟本篇“默认导出函数”这节中的 params 用法相同,用于获取动态路由参数,就不多说了。

generateImageMetadata 函数应该返回一个包含图片元数据的对象数组。每一个对象必须包含一个 id 值:

Image Metadata Object Type
id string(必传)
alt string
size { width: number; height: number }
contentType string

使用示例如下:

// icon.js
import { ImageResponse } from 'next/og'
 
export function generateImageMetadata() {
  return [
    {
      contentType: 'image/png',
      size: { width: 48, height: 48 },
      id: 'small',
    },
    {
      contentType: 'image/png',
      size: { width: 72, height: 72 },
      id: 'medium',
    },
  ]
}
 
export default function Icon({ id }) {
  return new ImageResponse(
    (
      <div
        style={{
          width: '100%',
          height: '100%',
          display: 'flex',
          alignItems: 'center',
          justifyContent: 'center',
          fontSize: 88,
          background: '#000',
          color: '#fafafa',
        }}
      >
        Icon {id}
      </div>
    )
  )
}

在这个例子中,我们生成了不同尺寸的图标图片。使用 generateImageMetadata 时,默认导出函数的 props 又会多一个 id 参数,可以根据 id 生成不同的图标内容。generateImageMetadata以数组形式定义了生成的图片数量和元数据,然后遍历该数组,将 id 参数传入默认导出函数,生成多张图片。

我们再举一个贴近实际开发的例子,假设一个产品有多张说明图,根据网址参数获取产品说明图数据,生成多张 Open Graph 图片:

// app/products/[id]/opengraph-image.js
import { ImageResponse } from 'next/og'
import { getCaptionForImage, getOGImages } from '@/app/utils/images'
 
export async function generateImageMetadata({ params }) {
  const images = await getOGImages(params.id)
 
  return images.map((image, idx) => ({
    id: idx,
    size: { width: 1200, height: 600 },
    alt: image.text,
    contentType: 'image/png',
  }))
}
 
export default async function Image({ params, id }) {
  const productId = params.id
  const imageId = id
  // 获取图片说明
  const text = await getCaptionForImage(productId, imageId)
 
  return new ImageResponse(
    (
      <div
        style={
          {
            // ...
          }
        }
      >
        {text}
      </div>
    )
  )
}

4. robots.txt

4.1. 介绍

robots.txt 用于告诉搜索引擎可以爬取网站中的哪些 URL。使用 robots.txt 也有两种方式,一种是使用静态文件,一种是使用代码生成。

4.2. 静态文件

app/ 目录下直接添加一个 robots.txt 即可:

User-Agent: *
Allow: /
Disallow: /private/
Sitemap: https://acme.com/sitemap.xml

4.3. 代码生成

添加一个 robots.jsrobots.ts文件,该文件导出一个 Robots对象。使用示例如下:

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

输出内容为:

User-Agent: *
Allow: /
Disallow: /private/
Sitemap: https://acme.com/sitemap.xml

如果使用 TypeScript,Robots 对象的 type 为:

type Robots = {
  rules:
    | {
        userAgent?: string | string[]
        allow?: string | string[]
        disallow?: string | string[]
        crawlDelay?: number
      }
    | Array<{
        userAgent: string | string[]
        allow?: string | string[]
        disallow?: string | string[]
        crawlDelay?: number
      }>
  sitemap?: string | string[]
  host?: string
}

5. sitemap.xml

5.1. 介绍

sitemap.xml,顾名思义,站点地图,用于帮助搜索引擎更高效的爬取网站。使用方式依然是两种,一种使用静态文件,一种使用代码生成。

5.2. 静态文件

app/sitemap.xml

<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>
  <url>
    <loc>https://acme.com/blog</loc>
    <lastmod>2023-04-06T15:02:24.021Z</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.5</priority>
  </url>
</urlset>

5.3. 代码生成

添加一个 sitemap.jssitemap.ts文件,该文件导出一个 Sitemap 对象。使用示例如下:

// app/sitemap.js
export default function 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,
    },
  ]
}

对应的输出为:

<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>
  <url>
    <loc>https://acme.com/blog</loc>
    <lastmod>2023-04-06T15:02:24.021Z</lastmod>
    <changefreq>weekly</changefreq>
    <priority>0.5</priority>
  </url>
</urlset>

如果使用 TypeScript,Sitemap 对象的 type 为:

type Sitemap = Array<{
  url: string
  lastModified?: string | Date
  changeFrequency?:
    | 'always'
    | 'hourly'
    | 'daily'
    | 'weekly'
    | 'monthly'
    | 'yearly'
    | 'never'
  priority?: number
}>

6. manifest.json

6.1. 介绍

开发 PWA 时会要求网站提供一个manifest.json文件设置网站的相关信息。使用方法依然是两种,一种静态文件,一种代码生成,直接看例子吧!

6.2. 静态文件

app/manifest.json | app/manifest.webmanifest

{
  "name": "My Next.js Application",
  "short_name": "Next.js App",
  "description": "An application built with Next.js",
  "start_url": "/"
  // ...
}

6.3. 代码生成

添加一个 manifest.jsmanifest.ts文件,该文件返回一个 Manifest 对象。使用示例如下:

// app/manifest.js
export default function manifest() {
  return {
    name: 'Next.js App',
    short_name: 'Next.js App',
    description: 'Next.js App',
    start_url: '/',
    display: 'standalone',
    background_color: '#fff',
    theme_color: '#fff',
    icons: [
      {
        src: '/favicon.ico',
        sizes: 'any',
        type: 'image/x-icon',
      },
    ],
  }
}

Manifest 对象具体有哪些字段参考 MDN 文档

参考链接

  1. Optimizing: Metadata
  2. Metadata Files: favicon, icon, and apple-icon
  3. Metadata Files: manifest.json
  4. Metadata Files: opengraph-image and twitter-image
  5. Metadata Files: robots.txt
  6. Metadata Files: sitemap.xml
  7. Functions: ImageResponse
  8. Functions: generateImageMetadata
© Copyright 2025 JackyLove 的技术人生. Powered with by CreativeDesignsGuru