Web Lukes
💻

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

公開日: 2025/1/30更新日: 2025/1/30

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

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

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

500 Internal Server Error 😱

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

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

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

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

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

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

症状一覧表

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

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

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

TypeScript
// 犯人はコイツだった(当時は知る由もない)
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日目:探偵ごっこが始まった

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

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

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

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

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

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

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

TypeScript
// この時の私の推測(外れ)
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方式:「最初から家に持ち込んでおく」→ 確実!

解決のひらめき

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

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

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

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

TypeScript
// 最初からアプリに組み込んでおく
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件」って表示されてたやつです。

TypeScript
// ❌ ビフォー:またもや迷子方式
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つのファイルを作ることにしました。

JavaScript
// 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 → 「絶対に使う」メイン用

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

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

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

Bash
# 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設定の最適化

TypeScript
// 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",
});

📊 問題解決の結果

パフォーマンス改善

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

デプロイ安定性

YAML
修正前:
- fetchリクエストの失敗リスク
- 静的ファイル配信との競合
- ランタイムエラーの可能性
修正後:
- ビルド時エラー検出
- 静的バンドルによる安定性
- キャッシュ効率の向上

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

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

Bash
# 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デプロイ履歴の活用

Bash
# デプロイ履歴の確認
npx wrangler pages deployment list --project-name web-lukes-remix
# 特定デプロイの詳細確認
# Cloudflare Dashboard でビルドログを確認

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

TypeScript
// 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. エラーハンドリング戦略

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

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

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

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

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

TypeScript
// 環境別の処理分岐
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. 堅牢なエラーハンドリング

TypeScript
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. 継続的監視の実装

TypeScript
// パフォーマンス監視
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にアクセスして、記事がちゃんと表示された時の**「やったー!!!」**という叫び。

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

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

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

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


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

🎁おすすめツール・サービス

FX自動売買VPSなら50%OFF

キャンペーンで月1,790円〜。MT4/MT5プリインストール済み、自動復旧機能付き。14日間無料お試し。

シンクラウドデスクトップ for FX
ゴトー日EAを探しているならソースコード付

4年で資金4.7倍のバックテスト実績。ソースコード付きで自由にカスタマイズ可能。

ゴトー日EA Ultimate版(3,980円)

※上記リンクにはプロモーション(広告)が含まれています

🔮
MEMBERSHIP

量子相場謀略|ホソノP

ブログでは書けない本音のトレード戦略とEA開発の裏側を公開。 データに基づいた検証で、再現性のある手法だけをお届けします。

✓EAソースコード公開
✓バックテスト生データ共有
✓リアルタイム相場分析
✓メンバー限定記事
メンバーシップに参加する※ noteアカウントで簡単登録

📚あわせて読みたい

💻

😭『なんで記事が反映されへんの!?』3時間の格闘から生まれたデプロイシステム完全改良記

「記事書いたのにサイトに出てこない...」そんな地獄から這い上がった、血と汗と涙のデプロイシステム改良プロジェクト。import地獄からfetch天国への道のり、全部見せます。

プログラミング・開発2025/1/31
💻

「また動いてない!💢」CSS ビルド・デプロイ時のスタイル適用問題を血と汗で解決した記録 😤 - Vite + Remix + Cloudflare

Vite ビルドシステムでのCSS生成問題とCloudflare Pages デプロイ時のスタイル適用エラーの原因と解決策。ファイルハッシュ不整合からクリーンビルドまで。

プログラミング・開発2025/1/30
💻

モダンブログサイトの技術アーキテクチャ全容解説 - Remix + Cloudflare + MDX

高速・低コスト・高機能を実現するブログサイトの技術スタック全容を解説。技術選定の理由から実装詳細、コスト分析まで。

プログラミング・開発2025/1/30
💻

😤『なんでタグクリックできないの!?』から始まったブログUI大改革の奇跡

ユーザビリティに問題だらけのブログサイトを1時間で劇的改善!タグのクリック機能、パンくずナビ、外部リンクの別タブ表示、スクロール位置記憶まで一気に実装した改善の全記録。

プログラミング・開発2025/1/31
💻

🎨 コードブロックを美しく!Prism.js シンタックスハイライト完全実装ガイド

Remix + Vite環境でPrism.jsを使ってコードブロックに美しいシンタックスハイライトを実装する方法。VS Codeテーマ風のカラーリングで開発者体験を向上!

プログラミング・開発2025/1/30
💻

😤『毎回全部ビルドするのもったいない!』から生まれたスマートデプロイシステムの奇跡

「記事1つ修正しただけなのに、なんで全サイトビルドしなきゃいけないの?」という怒りから生まれた、変更検知型スマートデプロイの開発秘話。時間もコストも90%削減した魔法のシステム。

プログラミング・開発2025/1/30