Astro + Cloudflare Pages プレビュー機能で500エラーが発生した原因と解決策
問題の概要
Astro + Cloudflare Pages で構築したNotionブログのプレビュー機能(SSR)で500エラーが発生。ローカル開発環境では動作するが、本番環境(Cloudflare Workers)では動作しない。
根本原因
Node.js専用モジュールの静的インポート
typescript
import { downloadImage } from '../image-downloader'; image-downloader.ts は node:fs と node:crypto を使用:
typescript
import fs from 'node:fs';
import crypto from 'node:crypto'; SSR時には downloadImage を呼び出さないようにしていたが、静的インポート自体がCloudflare Workersでモジュール解決エラーを引き起こす。
解決策
動的インポートに変更
typescript
type DownloadImageFn = (url: string) => Promise<string>;
let downloadImage: DownloadImageFn | null = null;
async function getDownloadImage(): Promise<DownloadImageFn> {
if (!downloadImage) {
const module = await import('../image-downloader');
downloadImage = module.downloadImage;
}
return downloadImage;
}
// 使用時
if (shouldDownload) {
const download = await getDownloadImage();
coverImage = await download(originalUrl);
} なぜ動的インポートで解決するのか
| 項目 | 静的インポート | 動的インポート |
|---|---|---|
| 評価タイミング | モジュール読み込み時 | 関数呼び出し時 |
| SSR時の挙動 | 必ずロードされる | 条件分岐でスキップ可能 |
| Cloudflare Workers | モジュール解決でエラー | エラー回避 |
追加の対応:Notion画像URL期限切れ問題
Notionの画像URLは約1時間で期限切れになる。プレビュー時は画像プロキシAPIを作成して対応:
typescript
/api/image-proxy.ts
export const GET: APIRoute = async ({ url, locals }) => {
const imageUrl = url.searchParams.get('url');
// 認証チェック後、画像をfetch → キャッシュ付きで返す
const response = await fetch(imageUrl);
return new Response(await response.arrayBuffer(), {
headers: {
'Cache-Control': 'public, max-age=3600',
},
});
}; 教訓
- Cloudflare Workersでは使えないNode.jsモジュールがある(fs, crypto, path等)
- 静的インポートは「呼び出さなくても」エラーになる
- 環境依存のモジュールは動的インポートを使う
- ローカルで動いても本番で動くとは限らない(ランタイムの違い)