如何在 Next.js 中生成包含中文的 Open Graph 图片
7 分钟阅读

如何在 Next.js 中生成包含中文的 Open Graph 图片

使用 next/og 生成 Open Graph 图片,同时解决中文字体体积过大的问题

最近在升级我的博客,想要在 Twitter 等平台上分享文章时,能够显示文章的封面图,于是就想到了 Open Graph 图片。

什么是 Open Graph 图片

Open Graph 图片是一种特殊的图片,它可以在 Twitter 等平台上显示,可以让你的文章在分享时更加美观。

如何在 Next.js 中生成 Open Graph 图片

Vercel 官方提供了一个 @vercel/og 的库,可以用来生成 Open Graph 图片。

下面是一个简单的用于 Next.js 14 App Router 的示例:

  1. 安装依赖
bash
1pnpm add @vercel/og
  1. 新增一个接口用于生成 Open Graph 图片
tsx
1// app/api/og/route.tsx 2import { ImageResponse } from 'next/og' 3 4export const runtime = 'edge' 5 6export function GET() { 7 return new ImageResponse( 8 ( 9 <div tw="flex flex-col w-full h-full p-[50px] justify-between bg-zinc-50"> 10 👋 Hello 11 </div> 12 ), 13 { 14 width: 1200, 15 height: 630, 16 }, 17 ) 18}

启动服务后,访问 http://localhost:3000/api/og,可以看到返回的图片:

如何动态生成图片

上面的示例中,我们只是简单的返回了一个静态的图片,但是在实际的项目中,我们需要根据文章的标题来生成图片。

我们可以从接口的参数中获取到文章的标题,然后将标题渲染到图片中。

tsx
1export function GET() { 2 const { searchParams } = new URL(req.url) 3 const title = searchParams.get('title') 4 5 return new ImageResponse( 6 ( 7 <div tw="flex flex-col w-full h-full p-[50px] justify-between bg-zinc-50"> 8 <h1 className="text-[80px] font-bold">{title}</h1> 9 </div> 10 ), 11 { 12 width: 1200, 13 height: 630, 14 }, 15 ) 16}

访问 http://localhost:3000/api/og?title=Hello, Hayden,可以看到返回的图片:

如何兼容中文

By default, @vercel/og only has the Noto Sans font included. If you need to use other fonts, you can pass them in the fonts option.

默认情况下,@vercel/og 只包含了 Noto Sans 字体,这是一个英文字体,不支持中文。所以我们需要自己传入支持中文的字体。

我们以 Mi Sans 字体为例:

  1. 将字体文件放到 public/fonts 目录下
txt
1|- public 2 |- fonts 3 |- MiSans-Regular.ttf
  1. 在接口中引入字体
tsx
1// ... 2// 这里的路径是相对于当前文件的 3const miSansFont = await fetch(new URL('../../../../public/fonts/MiSans-Regular.ttf', import.meta.url)).then((res) => 4 res.arrayBuffer(), 5) 6 7return new ImageResponse( 8 // ... 9 { 10 width: 1200, 11 height: 630, 12 fonts: [ 13 { 14 name: 'mi-sans', 15 data: miSansFont, 16 style: 'normal', 17 }, 18 ], 19 emoji: 'twemoji', 20 }, 21)

然后我们访问 http://localhost:3000/api/og?title=Hello, 启封Hayden,可以看到返回的图片:

如何解决中文字体体积过大的问题

上面的代码在本地运行是没有问题的,但是在部署到 Vercel 时,会报错:

这是因为部署时,Vercel 会将字体文件复制到 Edge Function 的运行环境中,免费版的 Edge Function 的运行环境只有 1MB,而 Mi Sans 字体压缩后也会超过 5M,所以会报错。

中文字体文件普遍都在 5M 以上,要解决这个问题一个方案是将文件上传到 CDN,然后在 Edge Function 中引入 CDN 上的文件。如此一来,Edge Function 的运行环境中就不会包含字体文件。

我试过一版,因为全量的字体文件比较大,放到 CDN 上,有一定概率 next.js 请求不到字体文件,导致部署失败。

解决中文字体包过大的问题

这时候我想起了 中文网字计划,他们有一个特色就是字体文件按需加载,也就是说,我们只需要加载我们需要的字体文件,而不是全量的。似乎可以解决我们的问题。

这里有两篇文章可以参考:

针对使用中文字体的网站优化方案有两个:

  1. 对全量的中文字体按一个规则进行切片,每个切片一版控制在 50k 以内,然后在网页加载时,浏览器依据当前用到的字符按需请求切片,这样就可以实现字体按需加载,减少字体文件的体积。
  2. 打包应用时,分析所有用到的文案,精准生成字体文件,这样就不会出现字体文件过大的问题。

方案 2 字体的体积更小,但是需要在打包时进行分析,也就是说对于动态生成的内容,无法应用这个方案。所以在大多数网站中,方案 1 更加实用。Google Fonts 就是使用的这个方案。

但是我们只想生成博客里标题需要的字体,而标题在博客部署时肯定是确定的。所以方案 2 更适合我们的场景。并且生成的字体文件体积也非常小,可以直接放到 Edge 中。

生成动态字体文件

我们在项目根目录下创建一个 js 文件:

js
1// scripts/compressFont.js 2import Fontmin from 'fontmin'; 3import * as glob from 'glob'; 4import matter from 'gray-matter' 5 6// 读取 content 下的所有 markdown 文件(包含子目录) 7// 使用 'gray-matter' 解析 markdown 文件,提取出 front matter 8// 将 front matter 中的 title 和 description 字段提取出来 9const markdownFiles = glob.sync('content/**/*.md'); 10const frontMatters = markdownFiles.map(file => matter.read(file).data); 11const textSubset = frontMatters.map(({ title, description }) => `${title}${description}`).join(''); 12 13const fontmin = new Fontmin() 14 .src('public/fonts/MiSans-Regular.ttf') 15 .use(Fontmin.glyph({ 16 text: textSubset, 17 hinting: false, 18 })) 19 .dest('public/fonts/dynamic'); 20 21fontmin.run((err, files) => { 22 if (err) throw err; 23 console.log('compress font success\n'); 24})

我们在 package.json 中添加一个脚本,用于生成动态字体文件:

json
1{ 2 "scripts": { 3 "compress-font": "node scripts/compressFont.js" 4 } 5}

最终生成的字体文件在 public/fonts/dynamic 目录下,我们将接口中的字体文件路径替换为动态字体文件路径即可。

tsx
1const miSansFont = await fetch(new URL('../../../../public/fonts/dynamic/MiSans-Regular.ttf', import.meta.url)).then( 2 (res) => res.arrayBuffer(), 3)

最后可以看到动态生成的字体文件只有 34k。

代码地址: