graph-igo

OGP 画像をビルド時に Skia で描く

この記事を SNS に貼ると、タイトルの上に一枚の画像が出る。その画像は dist/og/ に書き出された PNG で、ビルドのときにこのサイト自身が描いている。

画像を描く道具の選び方

ページを実際に開いて撮る方法がある。ヘッドレスブラウザでスクリーンショットを撮れば、見た目はページそのものになる。そのかわり CI がブラウザを一つ抱えることになり、生成にかかる時間もブラウザの起動時間に縛られる。

このサイトは canvaskit-wasm を使う。ブラウザが文字を並べるのに使っている Skia を、WebAssembly に載せたものである。長方形に文字を置くだけなら描画ライブラリは何でもよいが、置くのがタイトルとなるとそうはいかない。タイトルの長さは決まっていないのに、収める箱の幅は決まっている。字形を並べ、幅で折り返し、行数で打ち切って省略記号を付ける必要がある。Skia の paragraph API はそれをやる。

カードの文面はどこから来るか

ページのモジュールは title と description を export している。<head> に入るのと同じ値なので、カードもそこから作る。

const image = ogImage(routes.showcase.href(), Showcase);

この一回の呼び出しが、カードの描き方を記録し、<meta property="og:image"> に入れる URL を返す。登録せずに URL だけ受け取ることも、URL を受け取らずに登録することもできない。カードのためにページの一覧をもう一つ書けば、いつか片方だけが更新される。

クロールが辿れない画像

このサイトの静的ビルドは、router.tsx をそのまま fetch() して、返ってきた HTML の中のリンクを辿って広がっていく。辿り着けるものが生成されるものである。

カードはそこから漏れる。og:image はリンクを表示する側のサーバーが取りに来る URL なので、絶対 URL で書く。クローラーがそれを辿ったら、このサイトの外に出てしまう。

そこで router.tsx の entryPoints がカードの一覧を持つ。

export const entryPoints: readonly string[] = ["/", ...ogPaths()];

ogPaths() が読む登録簿は、ルートを配線するときに埋まる。だからこの export はファイルの末尾にある。

日本語のカード

Skia には CSS の font-family に当たる仕組みがないので、フォントの実体を渡す。server/og/fonts/ に置いた .ttf がファイル名順に登録され、Skia は一文字ずつその順に字形を探す。

Inter が先に並び、Noto Sans JP が後に続く。だから日本語の見出しに HTML が混ざっても、かなと漢字は Noto、HTML は Inter のまま出る。

日本語のウェイトは Regular だけを置いてある。太字の face を持たないファミリには、Skia が合成の太字をかける。本物の太字をもう一枚積めば 2MB 増えるので、この大きさなら合成で足りると判断した。

Noto Sans JP は JIS X 0208 に絞ってある。かなと記号、第 1 水準と第 2 水準の漢字 6,355 字で 2.2MB になる。フォント全体なら 5.3MB である。

絞ったぶん、外れる字がある。鷗 のように JIS X 0213 にしかない字は、このフォントに字形がなく、空白として描かれる。名前から一文字消えていても、誰も開かない画像の上でのことである。だからビルドが言う。

og: no glyph for 鷗 in /blog/mori-ogai — see server/og/fonts/README.md

エラーではなく警告にしてある。一文字のためにデプロイを落とす理由はないし、直し方はコードではなくフォントのファイルだからである。フォントを置く場所はこのディレクトリしかないので、全体版に差し替えれば警告は消える。

← All posts
Share this post