😱 Remixサイトが真っ白に!デプロイ地獄から這い上がった3日間の記録
😭 いきなり絶望からのスタート
「よし!ついにブログサイトが完成した!」
そう思って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-remixVite設定の最適化
// 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 devcurl http://localhost:5173/articles/test-article
# Step 2: ビルド環境での確認npm run buildls -la build/client/data/
# Step 3: デプロイ環境での確認curl https://xxx.pages.dev/data/articles.jsoncurl https://xxx.pages.dev/articles/test-article2. 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日間、本気で諦めようと思いました。
でも大丈夫!解決策はシンプル
✨ 今回学んだこと:
- 🔄 fetch → import: 「外に取りに行く」から「最初から持ち込む」へ
- 🌍 環境の違い: ローカルと本番は別世界だと心得よ
- 🕵️ 原因究明: 一歩ずつ、焦らず確実に
💪 次回への教訓:
- テスト: 小さくデプロイして確認
- ログ: エラーメッセージを恥ずかしがらずに読む
- 助け: わからないことは素直に調べる・聞く
🚀 あなたのサイトが動く瞬間の感動
修正してデプロイボタンを押した時の心臓のドキドキ。
URLにアクセスして、記事がちゃんと表示された時の**「やったー!!!」**という叫び。
きっとあなたも体験できます。
諦めないでください。
あなたの作ったサイトが世界中の人に見てもらえる日は近いです。
この記事が、あなたの助けに少しでもなれば幸いです。
一緒に頑張りましょう! 🌟
もし質問があれば、遠慮なくコメントしてくださいね。一緒に解決しましょう!
🎁おすすめツール・サービス
※上記リンクにはプロモーション(広告)が含まれています
量子相場謀略|ホソノP
ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。