ブログのOGP生成をAWS Lambdaを使って自動化しました。
Honoを使ってみる
AWS Lambdaではサービス固有のハンドラを書く必要がありますが、ベンダーロックインされたコードを書きたくなかったのでHonoを使ってエンドポイントを定義しました。 AWS Lambda用ガイドに従うと簡単に雛型が作成できます。
import { Hono } from "hono";import { handle } from "hono/aws-lambda";import { healthHandler } from "./handlers/health";import { getOgImageHandler } from "./handlers/og";import { logger } from "hono/logger";
export const app = new Hono();
app.use(logger());
app.get("/", (c) => c.text("Hello Hono!"));
app.get("/health", healthHandler);
app.get("/og", getOgImageHandler);
export const handler = handle(app);
export default app;AWS CLIのインストールと設定もしておきます。
aws configureSatori+resvgでOGPを自動生成する
SatoriというJSXをSVGにレンダーするライブラリと、resvgというSVGをPNGに変換するライブラリ(Rust実装)を組み合わせる方法が主流のようなのでこれを使います。@vercel/og(Vercelが提供しているOGP生成ライブラリ)と同じ構成ですね。
let isWasmInitialized = false;
...
if (!isWasmInitialized) { console.log("wasm initializing...");
const wasmPath = path.resolve( process.cwd(), process.env.NODE_ENV == "production" ? "index_bg.wasm" : "node_modules/@resvg/resvg-wasm/index_bg.wasm", ); const wasmBuffer = await fs.readFile(wasmPath);
await initWasm(wasmBuffer); isWasmInitialized = true; console.log("wasm initialized");}
...
const svg = await satori(<OgImage title={title} />, { width: 1200, height: 630, fonts: fontData,});
const resvg = new Resvg(svg, { fitTo: { mode: "width", value: 1200, },});const image = resvg.render();return c.body(Buffer.from(image.asPng()), 200, { "Content-Type": "image/png",});
...Satoriの方はすんなり動きましたが、resvgはRust実装のためAWS Lambdaで動かそうと思うと少し工夫がいります。npm packageにはバイナリ版とWasm版が存在していますが、手元のWindowsとAWS Lambda (Linux) 両方で動くWasm版を選択しました。
tsファイルのビルドにはesbuildを使用しました。zipファイルにまとめる前にesbuild-copy-pluginを使ってフォントや画像などのアセットやWasmバイナリをdistフォルダにコピーするようにしています。
また、開発環境と本番環境でパスが違うファイル(フォントや画像、wasmファイル)はこんな感じにNODE_ENV環境変数で切り替えています。(NODE_ENVはAWS Lambdaではデフォルトで設定されているわけではないので、手動で設定する必要があります。)
const wasmPath = path.resolve( process.cwd(), process.env.NODE_ENV == "production" ? "index_bg.wasm" : "node_modules/@resvg/resvg-wasm/index_bg.wasm",);
開発用サーバーをBunで立てる
JS/TS用統合ツールキットであるBunを使うとViteなどのパッケージを別途導入せずとも開発サーバーを立てられます。
{ "scripts": { "dev": "bun run --hot src/index.ts" }}npmなど他のパッケージマネージャより高速に動作し、開発サーバーやパッチといった開発に必要な機能が一つにまとまっているのがいいですね。
改行位置を調整したい
長いタイトルを入れるとこんな感じに変な位置で改行されてしまいます。

これを直すために、BudouXというGoogleが出している自然な改行位置で文章を分割してくれるライブラリを使ってみます。
bun install budouximport { loadDefaultJapaneseParser } from "budoux";
...
const parser = loadDefaultJapaneseParser();
...
<div style={{ fontSize: 60, fontFamily: "Zen Maru Gothic", background: "white", borderRadius: 24, color: "black", padding: "48px 72px", display: "flex", flexWrap: "wrap", flex: "1", }}> {parser.parse(title ?? "").map((chunk) => ( <span>{chunk}</span> ))}</div>
...budouxの中身は非常に軽量な機械学習モデル(15KBほど)なのでAWS Lambdaにも余裕で組み込めます。
結果はこんな感じ

AWS Lambdaにデプロイする
作ったものをAWS Lambdaにデプロイします。
bun run deployこれだけでビルドされたjsファイルがアセットとともにzipファイルに圧縮されてデプロイされます。
Lambdaへのアクセスですが、今回は自分だけがアクセスできれば良いのでAPI Gatewayは使わずLambdaの関数URL機能を使います。
認証付きの関数URLを作成してcurlしてみます。
export AWS_ACCESS_KEY_ID=AKIA...export AWS_SECRET_ACCESS_KEY=secret...curl --request GET \--aws-sigv4 "aws:amz:<AWSのリージョン>:lambda" \--user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \"https://<関数URL>/og?title=hogehoge" \> ogp.pngAWS Lambdaの設定はアーキテクチャ: arm64, メモリ: 1024MBにしました。1~3秒ぐらいで画像が生成されるので十分でしょう。
あとがき
今回OGPを自動生成するにあたって参考にした記事を貼っておきます。
また、今回出てきたコードはここに置いてあります。
最近ブログを書くのに使っているFrontmatter CMSと連携するとさらに快適にブログが書けそうな気がしています。近々それについての記事も出す予定です。