Cloudflare PagesでOGP画像を動的生成する際のWASM制限とその解決策
はじめに
Astro + Notion + Cloudflare Pages構成でブログを運用していた際、OGP画像の動的生成がローカル開発環境では動作するのに、本番環境では全く動かないという問題に遭遇しました。
本記事では、この問題の原因と解決策を共有します。
問題の発生
構成
- フレームワーク: Astro (Hybrid mode)
- ホスティング: Cloudflare Pages
- OGP生成: satori + @resvg/resvg-wasm(SSRで動的生成)
エラー内容
本番環境でOGP画像にアクセスすると、以下のエラーが発生:
WebAssembly.instantiate(): Wasm code generation disallowed by embedder
原因
Cloudflare Pages Functionsには、WASMの実行に制限があることが原因でした。
具体的には、Cloudflare Workers/Pages Functionsでは、セキュリティ上の理由からWebAssembly.instantiate()の使用が制限されています。resvg-wasmはSVGをPNGに変換するためにWASMを使用しており、この制限に抵触していました。
なぜローカルでは動いたのか
ローカル開発環境(Node.js)にはこの制限がないため、問題なく動作していました。Cloudflare特有の制限であるため、デプロイして初めて発覚する厄介な問題です。
解決策
アプローチ: SSR → SSG への変更
ランタイムでのOGP生成を諦め、ビルド時に静的ファイルとして生成する方式に変更しました。
// src/pages/og/[slug].png.ts
import type { APIRoute, GetStaticPaths } from 'astro';
import satori from 'satori';
import { Resvg, initWasm } from '@resvg/resvg-wasm';
import { getPosts } from '@/lib/notion/client';
// ビルド時に静的生成(SSG)
export const prerender = true;
// WASM初期化フラグ
let wasmInitialized = false;
async function ensureWasmInitialized(): Promise<void> {
if (wasmInitialized) return;
const fs = await import('node:fs/promises');
const path = await import('node:path');
const wasmPath = path.resolve(
process.cwd(),
'node_modules/@resvg/resvg-wasm/index_bg.wasm'
);
const wasmBuffer = await fs.readFile(wasmPath);
await initWasm(wasmBuffer);
wasmInitialized = true;
}
export const getStaticPaths: GetStaticPaths = async () => {
const posts = await getPosts();
return posts.map((post) => ({
params: { slug: post.slug },
props: { title: post.title },
}));
};
export const GET: APIRoute = async ({ props }) => {
const { title } = props as { title: string };
await ensureWasmInitialized();
// satoriでSVGを生成
const svg = await satori(
// React-likeなJSXオブジェクト
{
type: 'div',
props: {
style: { /* スタイル */ },
children: title,
},
},
{
width: 1200,
height: 630,
fonts: [{ name: 'Noto Sans JP', data: fontData }],
}
);
// resvgでPNGに変換
const resvg = new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } });
const pngData = resvg.render();
const pngBuffer = pngData.asPng();
return new Response(pngBuffer, {
headers: { 'Content-Type': 'image/png' },
});
}; ポイント
export const prerender = true;: これによりビルド時に静的ファイルとして生成されるgetStaticPathsで全記事のslugを列挙: ビルド時に必要なパスを事前に定義- WASMはNode.js環境で実行: ビルドはNode.jsで行われるため、WASM制限なし
追加で遭遇した問題
フォント形式の問題
satoriはWOFF2フォーマットに対応しておらず、以下のエラーが発生:
Unsupported OpenType signature wOF2
解決策: WOFF形式のフォントを使用
const fontUrl = 'https://cdn.jsdelivr.net/npm/@fontsource/noto-sans-jp@5.0.19/files/noto-sans-jp-japanese-700-normal.woff';
OGPパスの変更
SSR時代は /api/og/[slug].png だったパスを、SSG化に伴い /og/[slug].png に変更。SEOコンポーネントでのパス参照も更新が必要でした。
メリット・デメリット
SSG方式のメリット
- Cloudflare Freeプランで動作
- エッジでの処理不要、CDNキャッシュが効く
- レスポンス速度が高速
SSG方式のデメリット
- 記事追加時に再ビルドが必要
- ビルド時間が増加(記事数に比例)
- 動的なパラメータ対応が困難
まとめ
Cloudflare PagesでWASMを使用する際は、ランタイム制限を理解し、必要に応じてSSG方式を検討することをおすすめします。