Astro 7とsatori 0.33でOGP画像生成をメンテし直した
ずいぶん放置してしまいました、約3年半ぶりの記事です。
以前Astroのエンドポイントを使ってVercel上でOGP画像を自動生成してみたという記事を書いていました。今回Astroやsatoriのバージョンを上げたところ、OGP画像の生成が動かなくなってしまいました。
今回はその時に修正した内容のまとめです。
アップデート内容
大分放置していたので、下記のようにアップデートすることになりました。
| ライブラリ | 変更前 | 変更後 |
|---|---|---|
| Astro | 5系 | 7系 |
| @astrojs/vercel | 8系(@astrojs/vercel/serverless) |
11系(@astrojs/vercel) |
| satori | 0.26系 | 0.33系 |
それぞれで地味に破壊的な変更があり、特にOGP生成が500エラーを返すようになっていました。
1. getではなくGETでエクスポートする
今のAstroではHTTPメソッド名を大文字でエクスポートします。旧記事のexport function get()は動かないので、GETに変更します。
export async function GET({ params, url }: APIContext) {
// ...
}
2. output: "hybrid"は無くなった
hybridはAstro 5で廃止され、staticに統合されました。オンデマンド生成にしたいルートだけprerender = falseを付けます。旧記事とは既定の向きが逆になっているので注意が必要です。
export const prerender = false;
アダプタのimportも変わりました。
import vercel from "@astrojs/vercel";
3. Content Layer API への移行
記事の取得まわりも変わっています。OGP側で使っているgetEntryBySlugはgetEntryに。
import { getEntry } from "astro:content";
const entry = await getEntry("blog", params.slug || "");
あわせて一覧・詳細ページ側ではentry.slugがentry.idに、entry.render()がrender(entry)になっています。地味ですが移行しないと動きません。
4. runtime: "edge"は(今は)効かない
@astrojs/vercel v11ではルートごとのruntime設定は無視され、実体はNodeのサーバーレス関数になります。以前付けていた以下の設定は削除しました。
export const config = {
runtime: "edge",
};
node:fsが使えるので、resvg-wasmのwasmファイルを読み込む用途にはむしろ都合が良いです。
5. satori 0.33のSSRF保護で「自分のURL」が読めなくなった
一番ハマったのがここです。以前は背景画像を次のようにリクエスト元のURLで渡していました。
const dataUri = `${url.origin}/content/background.jpg`;
ところがsatori 0.33ではSSRF対策が入り、localhostやプライベートアドレスへの画像取得がブロックされます。開発環境ではurl.originがhttp://localhost:4321になるため、そのまま500エラーになってしまいました。
対策として、背景画像はViteの?inlineでビルド時にdata URI化して埋め込みます。URLを渡さないのでSSRFの対象になりません。
import backgroundImage from "../assets/background.jpg?inline";
// ...
const dataUri = backgroundImage;
記事ごとのサムネイルがある場合は、いったんfetchで取得してbase64のdata URIへ変換してからsatoriに渡します。
const toDataUri = async (source: string) => {
const res = await fetch(source);
if (!res.ok) {
throw new Error(`Failed to fetch image: ${source}`);
}
const contentType = res.headers.get("content-type") ?? "image/jpeg";
const buffer = Buffer.from(await res.arrayBuffer());
return `data:${contentType};base64,${buffer.toString("base64")}`;
};
const { thumbnail } = entry.data;
const image = thumbnail
? {
src: await toDataUri(`${url.origin}${thumbnail.src}`),
width: thumbnail.width,
height: thumbnail.height,
}
: { src: backgroundImage, width: 1800, height: 1200 };
6. Responseに渡す前にUint8Arrayにする
asPng()が返すBufferをそのままnew Response()に渡すと環境によって問題になることがあるため、Uint8Arrayに包み直します。
const pngBuffer = pngData.asPng();
return new Response(new Uint8Array(pngBuffer), {
headers: {
"content-type": "image/png",
"cache-control": "public, immutable, no-transform, max-age=31536000",
},
});
7. satoriが使うharfbuzzjsのwasmが本番で見つからない
satori 0.33では文字のシェーピングにharfbuzzjsを使うようになり、実行時にnode_modules/harfbuzzjs/hb.wasmをfsで読み込みます。
ローカルではnode_modulesがあるので問題ありませんが、Vercelの関数には自動で同梱されないため、本番で次のエラーが出て500になりました。
ENOENT: no such file or directory, open '/var/task/node_modules/harfbuzzjs/hb.wasm'
resvg-wasmのときと同じで、アダプタのincludeFilesに追加すれば解決します。
adapter: vercel({
includeFiles: [
"./node_modules/@resvg/resvg-wasm/index_bg.wasm",
"./node_modules/harfbuzzjs/hb.wasm",
],
}),
まとめ
アップデートでOGP生成が壊れる要因は、大きく分けると次の3つでした。
- Astro側のAPI・設定の変更(メソッド名、
output、Content Layer API) - satoriのSSRF保護で画像のURL取得が使えなくなった
- satoriが使うwasm(harfbuzzjs)がVercelの関数に同梱されず本番で失敗した
特に後ろ2つは、ビルドは通るのに開発や本番で500になるタイプで気付きにくいので、同じ構成でOGP画像を生成している方の参考になれば幸いです。
