honoのブログにogp画像を自動生成して表示する
Published on: 2024/05/25
honoのブログにOGP画像を追加する方法を検討します。
検討案
- mdxのフロントマターで対応する
- satoriを使ってogp画像を自動生成する
mdxのフロントマターで対応する
MDXファイルの先頭の部分を「フロントマター(front matter)」と呼ばれています。
---
title: "タイトル"
description: "ブログの説明文"
date: "2024/05/23"
published: true
---
フロントマターは三つのダッシュ---
で囲まれており、YAML形式で記述されます。
ここにogpのimage_urlを記録するフィールドを追加し、build時にogp画像の設定を行えば良さそうです。
satoriを使ってogp画像を自動生成する
satoriはNext.jsなどで有名なVercel社が開発したHTML/CSSをsvgに変換するライブラリです。 このライブラリとresvgなどのSVGtoPNG変換系ライブラリを組み合わせることで自動でOGP画像を生成することができます。
先行例としてHonoXでsatoriを使ってOGイメージもSSGする や、kvnang/workers-ogなどがあります。
今回は、すでにhonoxでblogを作成しているため、Pages Functionsを使ってOGP画像の自動生成を実装します。 Pages FunctionsでのOGP自動生成はkvnang/workers-ogを使うと簡単ですが、このライブラリは日本語には対応してません。 そのため、このライブラリを改造して日本語にも対応したOGP自動生成を実現したいと思います。
ディレクトリ構成
完成したディレクトリ構成は以下のようになります。 lib/workers-ogは、kvnang/workers-ogのpackage/workers-og/srcのファイルをコピーしています。
functions
├── api
└── lib
├── vendors
└── workers-og
日本語フォント対応
workers-og/og.tsファイル内でGoogleフォントを指定して読み込んでいる箇所があります。 そこをNoto Sans JPを読み込むように変更します。
const svg = await satori(reactElement, {
...widthHeight,
fonts: !!options?.fonts?.length
? options.fonts
: [
{
name: "Noto Sans JP",
data: await loadGoogleFont({ family: "Noto Sans JP", weight: 600 }),
weight: 500,
style: "normal",
},
],
});
エンドポイントの対応
OGPの画像はGETリクエストでクエリパラメータで受け取った文字列を描画し、PNGファイルを返却するAPIとして実装します。 APIのエンドポイントを記述するファイルとして、functions/api/[[route]].tsファイルを作成します。
import { Hono } from "hono";
import { handle } from "hono/cloudflare-pages";
import { createOGPImage } from "./createOGPImage";
export const runtime = "edge";
const app = new Hono().basePath("/api");
app.get("/ogimg", createOGPImage);
export const GET = app.fetch;
export const POST = app.fetch;
export const onRequest = handle(app);
APIの作成は、Honoを利用しています。 Honoは、シンプルで早いWebフレームワークです。 書き方はExpressによく似ており、Expressを利用したことがあるなら、すぐにキャッチアップできると思われます。
OGP自動生成処理の作成
functions/api/createOGPImage.tsファイルを作成します。
import { ImageResponse } from "../lib/workers-og";
export const createOGPImage = async (c) => {
const title = c.req.query("title") || "Undefined";
const html = `
<div style="display: flex; justify-content: center; align-items: center; width: 1200px; height: 630px; background-color: #fff; font-family: "Noto Sans JP"">
<h1 style="font-size: 48px; color: #333; padding: 0 28px 0; display: flex;">${decodeURIComponent(
title
)}</h1>
<div style="display: flex; justify-content: center; align-items: center; font-size: 36px; color: #333; position: absolute; bottom: 16px;">
My Blog
</div>
</div>
`;
return new ImageResponse(html, {
width: 1200,
height: 630,
});
};
ImageResponseは前述したkvnang/workers-ogを流用しNoto Sans JPを利用するようにしたライブラリです。 このライブラリは、HTML文字列、width、heightを受け取ると、PNG画像をresponseで返却します。 responseの処理はライブラリ内で行いますので、c.renderなどの処理を呼び出す必要はありません。
API呼び出し側のコード
API呼び出し側では、metaタグでog:imageのURLを動的に生成します。
export default jsxRenderer(({ children, title, ogImage }) => {
const hostUrl = import.meta.env.PROD
? "https://example.com/"
: "http://localhost:5173/";
const ogImgTitle = title ? encodeURIComponent(title) : "";
const ogTitle = title
? title + " | My Blog"
: "My Blog";
ogImage = ogImage ? ogImage : hostUrl + "api/ogimg?title=" + ogImgTitle;
return (
<html lang="jp">
<head>
{import.meta.env.PROD ? <GoogleAnalytics /> : null}
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:title" content={ogTitle} />
<meta property="og:image" content={ogImage} />
// 以下、省略
)});
titleはencodeURIComponent関数でエスケープしないと、日本語をクエリパラメータで指定したときに、エラーが発生してOGP画像が生成されません。
resvg-wasmのバージョンについて
2024/05/26時点でresvg-wasmの最新バージョンは2.6.2です。 こちらのバージョンを利用すると、Cloudflare Workersの1MB制限(無料プラン)にwasmファイルのサイズが触れてしまいます。
そのため、resvg-wasmのバージョンを2.4.0固定にしないといけませんでした。
まとめ
このブログの内容で、OGPの自動生成を実現することができました。 冒頭のフロントマターも合わせて対応するとOGP画像の設定の自由度が高まるため、両方対応してしまうのが良いかと思われます。