Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Next.js Vercel node-canvas registerFont 2021

筆者は最近以下のウェブサイトを作りました。

everycolor.png

https://everycolor.vercel.app/

全ての色に名前を与えるライブラリ「everycolor」を試せるサイトです。

このサイトは今流行りのNext.jsで作り、Vercelにデプロイしています。各カラーコードに対するページが存在しており、次のようにOGP画像も表示されます。

この記事では、これを実装する際に詰まった点とその解決法をご紹介します。

Next.js + VercelでOGP画像を生成する手段

今回の場合、ページが$256^3 = 16,777,216$個存在しているため、リクエストが来るのに応じてページをSSRすることになります。これはNext.jsなら簡単ですね。また、各ページに異なるOGP画像が必要なので画像もやはり自動生成しなければいけません。そのための方法は状況に応じていくつかあります。次の記事によくまとまっています。

今回採用したのは、上の記事でいう案6です。すなわち、Next.jsのAPIルートとして画像を返すエンドポイントを用意しました。例えば上のツイートの画像は https://everycolor.vercel.app/api/image?code=8080ff です。これはアプリとは別にオブジェクトストレージなどを用意する必要がなくNext.js単体で完結するのも魅力的です。

この記事にもあるように、こうするとアプリサーバーへの負荷が無駄にかかる点が心配です。こういう場合の正攻法は画像生成サーバーの前段にCDNを置いてキャッシュしてもらうことなので、上のURLでも一応 Cache-Control: max-age=3600 を返していますが、APIルートだからということなのか、残念ながらVercel側でキャッシュしてくれるわけではないようです。

とはいえ、今回は目的がOGP画像であり、OGPの画像は普通はサービス側(Twitterとか)でキャッシュされるので今回はあまり気にしないことにします。

さて、Next.jsのAPIルートで画像を生成するには、Node.js上で画像を生成する手段が必要です。そのために使える強力なライブラリが node-canvasです。これはよく知られたHTMLのcanvas要素のAPIをNode.jsで使えるように移植したものです。さらに、サーバーから使いやすいようにフォントファイルからフォントを読み込めるなど独自機能も用意されています。

これを使えばローカルで画像生成の動作を確認するのは意外と簡単でした。しかし、Vercelにいざデプロイしてから結構苦労しました。

libuuid.so.1 が無いと言われる

VercelにデプロイしてAPIルートを叩いてみるとこんなエラーが記録されました。どうやらAPIルートが今流行りのサーバーレスやら何やらでLambdaにデプロイされており、その環境にライブラリが足りないことで起こる問題のようです。

Error: libuuid.so.1: cannot open shared object file: No such file or directory

これについては既存の日本語情報がありますので、これに従えば解消できます。

具体的には、1番目の記事に従って次のようなビルドスクリプトにすればOKでした。ビルド時にyum installしていいんですね。(しかも意外と速い)

yum install libuuid-devel libmount-devel && cp /lib64/{libuuid,libmount,libblkid}.so.1 node_modules/canvas/build/Release/ && next build

フォントが読み込めない

これでとりあえずnode-canvasが動いたのですが、出力される画像が文字化けしていました。おそらくフォントが無いのでしょう。ということで、node-canvasのregisterFontを用いてフォントを読み込む必要があります。node-canvasのドキュメントによれば、次のようにregisterFontにファイル名を渡すと読み込まれるようです。

registerFont('comicsans.ttf', { family: 'Comic Sans' })

Vercel上で動くようにするにはこのファイル名をどうすれば良いでしょうか。以下の記事にpath.resolveを使えば良いという情報があります。

例えば、Next.jsのディレクトリ構成のpublic(静的ファイルを置くところ)にExo.ttfを置いた場合、次のようにすればAPIルート(pages/api/image.ts)から読み込めるはずです。

registerFont(path.resolve('./public/Exo.ttf'), { family: 'Exo' })

しかし、筆者が試してみたところうまくいきませんでした。次のようなエラーメッセージが表示されます。

ENOENT: no such file or directory, lstat '/var/task/public'

つまり、path.resolve('./public/Exo.ttf')/var/task/public/Exo.ttfに解決された模様だが、そもそもpublicディレクトリが存在しないようです。

この問題に対する既存の解決策は調べた限り見つからなかったので、2021年版として筆者がとった解決策をメモしておきます。

Webpackでフォントファイルを埋め込む

APIルートのファイルから他のファイルを読み込むのはどうやらハードルが高いようです。そこで、今回はimage.tsにフォントファイルを埋め込むことにしました。

幸いにも、Next.jsではpages/api/image.tsのようなAPIルートもWebpackでビルドされます。そこで、Webpackの機能を使えばソースコードを汚すことなくフォントファイル埋め込むことができます。

具体的には、next.config.js内でWebpackのconfigを書き換えられる機能を用いて、.ttfファイルに対するルールを追加します。

  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    config.module.rules.push({
      test: /\.ttf$/,
      type: "asset/inline"
    });
    return config
  },

type: "asset/inline"というのはWebpack5で追加された機能で、このtypeが与えられたファイルをインポートするとData URIが得られます。Webpack 4を使っている場合はurl-loaderを使って同じことができます。

これをpages/api/image.ts側から読み込みます。コードの全体像が見たい方はGitHubを参照してください。

Webpackの設定を書いたので、APIルート側からは普通に.ttfファイルをimportします。

import fontDataURI from "../../../fonts/Exo.ttf";

これでフォントデータを無事手に入れることができましたが、node-canvasのregisterFontはファイル名を指定した読み込みしかできないようです。そこで、このデータをファイルに書き込みます。あらかじめ用意したファイルを読み込むのはそのファイルがどこに置かれているのか分からないため諦めましたが、自分で好き勝手なファイルに書き込むならば問題ないようです。

まず、data-uri-to-bufferを用いてdata URIをnode.jsのBufferオブジェクトに変換します。

import dataUriToBuffer from "data-uri-to-buffer";

const fontBuf = dataUriToBuffer(fontDataURI);

さらに、それを適当なファイルに書き込んで、そのファイルのパスをregisterFontに与えます。

import { mkdtempSync, writeFileSync } from "fs";
import { tmpdir } from "os";
import path from "path";

const td = mkdtempSync(path.join(tmpdir(), "everycolor"));
const fontFile = path.join(td, "Exo.ttf");
writeFileSync(fontFile, fontBuf);

registerFont(fontFile, {
  family: "Exo",
});

こうすることで、無事にnode-canvasにフォントを読み込ませることに成功しました。

フォントデータがJSファイルに埋め込まれるのがややサイズ的に心配ですが、今回はASCII文字のみ使用するためフォントサイズが大きくなかったのが幸いでした。

まとめ

Vercel上でpath.resolveを用いてもAPIルートからファイルが読み込めなかった場合は、Webpackにファイルを埋め込んでもらうのも手です。

uhyo
Metcha yowai software engineer
https://uhy.ooo/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away