😱 Remixサイトが真っ白に!デプロイ地獄から這い上がった3日間の記録

「ローカルでは動くのに本番で500エラー」あるある体験談。初心者の私がRemix + Cloudflare Pagesで地獄を見て、最終的に解決するまでの涙と汗の物語。

公開日: 2025-01-30
更新日: 2025-01-30

😭 いきなり絶望からのスタート

「よし!ついにブログサイトが完成した!」

そう思ってRemixで作ったサイトをCloudflare Pagesにデプロイした金曜日の夜。意気揚々とURLにアクセスしたら...

500 Internal Server Error 😱

は???ローカルでは完璧に動いてたのに!!!

この時の私の心境:「やっぱり私には無理だったんだ...」「月曜日に会社で報告するはずだったのに...」「なんで開発って毎回こうなるの?」

でも諦めるわけにはいかない。3日間、寝不足でコーヒーを飲みながら格闘した記録を、同じ地獄を味わっている人のために残します。

「あー、これ私も経験した!」 という人、一緒に解決していきましょう!

🔥 1日目:「なんでやねん」の連続

まずは冷静に症状を整理(するつもりだった)

症状一覧表

  • 😊 ローカル開発: サクサク動く、記事もちゃんと表示
  • 😨 Cloudflare Pages: 500エラーで真っ白画面
  • 😵 記事ページ: 個別ページ(/articles/何とか)全滅
  • 😓 ホームページ: 「記事が0件あります」→は???

最初は「まあ、ちょっとしたミスでしょ」って軽く考えてたんです。でも...

「動いてたコードが急に動かない」の謎

// 犯人はコイツだった(当時は知る由もない)
export const loader = async ({ params, request }) => {
  const url = new URL(request.url);
  
  // 「これでJSONファイル取れるでしょ?」と思ってた
  const res = await fetch(`${url.origin}/data/articles.json`);
  
  if (!res.ok) {
    throw new Response("記事データの取得に失敗しました", { status: 500 });
  }
  
  const data = await res.json();
  // ...
};

私の心の声:「fetchって、普通にファイル取ってくるだけでしょ?なんで500エラー?意味わからん」

この時はまだ、この1行が3日間の地獄の始まりだとは知りませんでした... 😅

🔍 2日目:探偵ごっこが始まった

「ファイルがない?そんなバカな...」

土曜日の朝、コーヒーを片手に再挑戦。まずは基本的なところから確認してみました。

# 恐る恐るアクセスしてみる
curl https://xxx.pages.dev/data/articles.json
# -> 404 Not Found

# えっ???

私の心の声:「は?だってローカルにはあるじゃん!」

# ローカルで確認
$ ls -la build/client/data/
# articles.json ← ちゃんとある!

まさか設定ファイル?(結果:違った)

「もしかして設定の問題?」と思って vite.config.ts を疑いました。

// この時の私の推測(外れ)
export default defineConfig({
  plugins: [
    tailwindcss(),
    remix({...}),
    tsconfigPaths(),
  ],
  // publicDir: "public", // これが足りないのかも?
});

publicDir: "public" を追加してデプロイ...

結果: まだ500エラー 😤

夜中の3時、ついに犯人発見

深夜のデバッグ風景

真相:Cloudflare PagesとRemixの「縄張り争い」だった!

想像してみてください。JSONファイルが宅配便だとすると:

  • 通常の宅配便(静的ファイル):「public/data/articles.json 宛ですね、はいどうぞ」
  • RemixのSSR(特急便):「待て待て、そのURLは俺が処理する!」

結果、宅配便が迷子になって404エラー 📦❌

「なるほど、そういうことか!」(この瞬間の達成感、忘れません)

🎯 3日目:ついに光が見えた!

「fetch やめて import にすればいいじゃん!」

日曜日の朝、ふとひらめきました。

宅配便の例えで説明すると:

  • fetch方式:「外に取りに行く」→ 道で迷子になる
  • import方式:「最初から家に持ち込んでおく」→ 確実!

解決のひらめき

💡 実際の修正:めちゃくちゃシンプル

ビフォー(迷子になる方式):

// 外にファイル取りに行って迷子になる
const res = await fetch(`${url.origin}/data/articles.json`);
const data = await res.json(); // ←ここで失敗してた

アフター(最初から持ち込む方式):

// 最初からアプリに組み込んでおく
try {
  const { default: articlesData } = await import("~/data/articles.generated.json");
  const data = articlesData; // ←確実に取れる!
  
  if (!article) {
    throw new Response("記事が見つかりません", { status: 404 });
  }
} catch (error) {
  throw new Response("記事データの読み込みに失敗しました", { status: 500 });
}

「なんだ、こんなシンプルなことだったのか!」

🏠 ホームページも同じように修正

同じ問題がホームページでも起きてました。「記事0件」って表示されてたやつです。

// ❌ ビフォー:またもや迷子方式
const res = await fetch(`${url.origin}/data/articles.json`);
if (!res.ok) return json({ articles: [] }); // 😭
const data = await res.json();

// ✅ アフター:持ち込み方式で安心
let data;
try {
  const { default: articlesData } = await import("~/data/articles.generated.json");
  data = articlesData; // 🎉 確実に取れる!
} catch (error) {
  // もしダメでも親切にエラー処理
  return json({ 
    articles: [], 
    tags: [], 
    // ... その他のデフォルト値
  });
}

📦 「両方作っちゃえ」作戦

せっかくなので、どっちでもアクセスできるように2つのファイルを作ることにしました。

// scripts/build-articles.mjs
// 「保険をかける」気持ちで両方生成
await Promise.all([
  fs.writeFile('public/data/articles.json', jsonContent),     // 静的ファイル用
  fs.writeFile('app/data/articles.generated.json', jsonContent) // import用(メイン)
]);

なぜ2つ作るの?

  • public/data/articles.json → 「もしかしたら使うかも」用
  • app/data/articles.generated.json → 「絶対に使う」メイン用

保険をかけておけば安心ですよね! 💪

🏗️ デプロイプロセスの改善

修正されたビルド・デプロイフロー

# 1. 記事データ生成
node scripts/build-articles.mjs
# → public/data/articles.json (静的配信用)
# → app/data/articles.generated.json (import用)

# 2. Remixビルド
npx remix vite:build
# → import用JSONがバンドルされる

# 3. Cloudflare Pagesデプロイ
npx wrangler pages deploy build --project-name web-lukes-remix

Vite設定の最適化

// vite.config.ts 完全版
export default defineConfig({
  plugins: [
    tailwindcss(),
    remix({
      future: {
        v3_fetcherPersist: true,
        v3_relativeSplatPath: true,
        v3_throwAbortReason: true,
        v3_singleFetch: true,
        v3_lazyRouteDiscovery: true,
      },
    }),
    tsconfigPaths(),
  ],
  publicDir: "public",
});

📊 問題解決の結果

パフォーマンス改善

const performanceComparison = {
  修正前: {
    記事ページ: "500エラー",
    ホームページ: "記事0件表示",
    読み込み方式: "Runtime fetch",
  },
  
  修正後: {
    記事ページ: "正常表示",
    ホームページ: "全記事表示",
    読み込み方式: "Build-time import",
    パフォーマンス向上: "静的バンドルによる高速化"
  }
};

デプロイ安定性

修正前:
  - fetchリクエストの失敗リスク
  - 静的ファイル配信との競合
  - ランタイムエラーの可能性

修正後:
  - ビルド時エラー検出
  - 静的バンドルによる安定性
  - キャッシュ効率の向上

🛡️ トラブルシューティング手法

1. エラーの段階的切り分け

# Step 1: ローカル環境での動作確認
npm run dev
curl http://localhost:5173/articles/test-article

# Step 2: ビルド環境での確認
npm run build
ls -la build/client/data/

# Step 3: デプロイ環境での確認
curl https://xxx.pages.dev/data/articles.json
curl https://xxx.pages.dev/articles/test-article

2. Cloudflareデプロイ履歴の活用

# デプロイ履歴の確認
npx wrangler pages deployment list --project-name web-lukes-remix

# 特定デプロイの詳細確認
# Cloudflare Dashboard でビルドログを確認

3. 段階的修正アプローチ

// Step 1: エラーハンドリングの強化
try {
  const res = await fetch(`${url.origin}/data/articles.json`);
  if (!res.ok) {
    console.error('Fetch failed:', res.status, res.statusText);
    throw new Error(`HTTP ${res.status}: ${res.statusText}`);
  }
  // ...
} catch (error) {
  console.error('Article loading error:', error);
  throw new Response("詳細なエラー情報", { status: 500 });
}

// Step 2: フォールバック機能の実装
const loadArticleData = async () => {
  try {
    // Method 1: Import
    const { default: articlesData } = await import("~/data/articles.generated.json");
    return articlesData;
  } catch (importError) {
    try {
      // Method 2: Fetch (fallback)
      const res = await fetch('/data/articles.json');
      return await res.json();
    } catch (fetchError) {
      // Method 3: Default data
      return {};
    }
  }
};

💡 学んだ教訓

1. プラットフォーム特性の理解

Cloudflare Pages の特徴:

  • 静的ファイルの配信優先度
  • Functions (SSR) との住み分け
  • エッジでのキャッシュ戦略

Remix の特徴:

  • ローダーでのサーバーサイド処理
  • ビルド時最適化の重要性
  • 動的インポートの活用

2. エラーハンドリング戦略

// 推奨: 詳細なエラー情報
throw new Response(`Article not found: ${slug}`, { 
  status: 404,
  statusText: 'Article Not Found' 
});

// 非推奨: 汎用的なエラー
throw new Response("エラー", { status: 500 });

3. デプロイ前のチェックリスト

  • ローカル開発環境での動作確認
  • ビルド生成物の確認
  • 静的ファイルの配置確認
  • エラーハンドリングのテスト
  • 段階的デプロイでの検証

🔧 予防策とベストプラクティス

1. 開発環境と本番環境の差異対策

// 環境別の処理分岐
const isProduction = process.env.NODE_ENV === 'production';
const isDevelopment = !isProduction;

if (isDevelopment) {
  // 開発時: リアルタイムMDX読み込み
  const { getArticleFromMDX } = await import("~/utils/markdown.server");
  article = await getArticleFromMDX(slug);
} else {
  // 本番時: 事前ビルドJSON読み込み
  const { default: articlesData } = await import("~/data/articles.generated.json");
  article = articlesData[slug];
}

2. 堅牢なエラーハンドリング

const safeArticleLoader = async (slug: string) => {
  try {
    const articleData = await loadArticleData();
    const article = articleData[slug];
    
    if (!article) {
      throw new Response(`Article "${slug}" not found`, { 
        status: 404,
        headers: { 'Content-Type': 'text/plain' }
      });
    }
    
    return article;
  } catch (error) {
    // ログ出力(本番環境ではログサービスに送信)
    console.error(`Article loading failed for ${slug}:`, error);
    
    if (error instanceof Response) {
      throw error;
    }
    
    throw new Response('Internal server error while loading article', { 
      status: 500 
    });
  }
};

3. 継続的監視の実装

// パフォーマンス監視
const trackArticleLoad = (slug: string, startTime: number) => {
  const duration = Date.now() - startTime;
  
  // Cloudflare Analytics や外部サービスに送信
  if (typeof window !== 'undefined') {
    navigator.sendBeacon('/api/metrics', JSON.stringify({
      event: 'article_load',
      slug,
      duration,
      timestamp: Date.now()
    }));
  }
};

🎉 あなたへのメッセージ(同じ地獄を味わっている人へ)

このエラーで悩んでいるあなたへ

今この記事を読んでいるということは、きっと私と同じような絶望を味わっているんだと思います。

「ローカルでは動くのになんで!?」
「もう疲れた...」
「自分には無理なのかも...」

その気持ち、痛いほどわかります。私も3日間、本気で諦めようと思いました。

でも大丈夫!解決策はシンプル

✨ 今回学んだこと:

  1. 🔄 fetch → import: 「外に取りに行く」から「最初から持ち込む」へ
  2. 🌍 環境の違い: ローカルと本番は別世界だと心得よ
  3. 🕵️ 原因究明: 一歩ずつ、焦らず確実に

💪 次回への教訓:

  • テスト: 小さくデプロイして確認
  • ログ: エラーメッセージを恥ずかしがらずに読む
  • 助け: わからないことは素直に調べる・聞く

🚀 あなたのサイトが動く瞬間の感動

修正してデプロイボタンを押した時の心臓のドキドキ。
URLにアクセスして、記事がちゃんと表示された時の**「やったー!!!」**という叫び。

きっとあなたも体験できます。

諦めないでください。
あなたの作ったサイトが世界中の人に見てもらえる日は近いです。

この記事が、あなたの助けに少しでもなれば幸いです。

一緒に頑張りましょう! 🌟


もし質問があれば、遠慮なくコメントしてくださいね。一緒に解決しましょう!

この記事が役に立ったらシェアしてください

📚 プログラミング・開発 の関連記事