Astro + Cloudflare Pages プレビュー機能で500エラーが発生した原因と解決策

問題の概要

Astro + Cloudflare Pages で構築したNotionブログのプレビュー機能(SSR)で500エラーが発生。ローカル開発環境では動作するが、本番環境(Cloudflare Workers)では動作しない。

根本原因

Node.js専用モジュールの静的インポート

typescript
import { downloadImage } from '../image-downloader';

image-downloader.tsnode:fsnode: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',
    },
  });
};

教訓

  1. Cloudflare Workersでは使えないNode.jsモジュールがある(fs, crypto, path等)
  2. 静的インポートは「呼び出さなくても」エラーになる
  3. 環境依存のモジュールは動的インポートを使う
  4. ローカルで動いても本番で動くとは限らない(ランタイムの違い)