メインコンテンツまでスキップ

ローカルでは動くのに、デプロイ後に壊れる? たいてい原因はパスです

よくあるのに、まったく面白くないバグがあります。

ローカル開発では全部まともに見えるのです。

  • ページは開く
  • 画像も出る
  • CSS も生きている
  • リンクも一応仕事をしている

ところがデプロイした瞬間、サイトが急に芸を始めます。

  • 画像が 404
  • JS chunk が読めない
  • CSS のパスが消える
  • 下層ページは開けるのに、リロードすると死ぬ
  • /docs/intro はクリックすれば開くのに、直接開くとサーバーが知らないふりをする

このとき多くの人はこう言います。

でもローカルでは動いていました。

ええ。

ローカル環境は、あなたのミスを甘やかすのが得意です。

本当の問題は「コードが突然気分で壊れた」ことではありません。

たいていは、ローカルでしか成立しない前提を、そのまま本番に持ち込んだだけです。

その中でも特に多いのがこれです。

パス。

もっと雑に言うと、ファイルは A にあるのに、ブラウザには B を見に行かせている、という話です。

なぜこんなに起こるのか

ローカル開発環境はだいたい寛容だからです。

  • dev server がルーティングのミスを雑に吸収する
  • サイトがルート / 配下で動いていることが多い
  • CDN もリバースプロキシもサブディレクトリ配置もない
  • キャッシュが軽いので、ミスが長生きしにくい
  • ローカルでは深い URL を直打ちせず、だいたいクリックで移動する

だからパスまわりの問題は、本番に出るまで露出しないことが多いのです。

本番で少し条件が変わると、サイトは無言で 404 を返してきます。

よくある変化はこんなものです。

  • サイトが / ではなく /blog//docs/ の下に載る
  • 静的アセットに CDN プレフィックスが付く
  • サーバーが SPA fallback をしてくれない
  • リバースプロキシが前置パスを削る、または間違って書き換える
  • 「絶対パス」のつもりが、実際には「ドメインルート基準のパス」だった

これだけで十分壊れます。

典型的な罠:/img/a.png

まずはこういう書き方です。

![cover](/img/cover.png)

あるいは JSX でこう。

<img src="/img/cover.png" alt="cover" />

サイトが次の場所にあるなら:

https://example.com/

たいてい問題ありません。

でも実際の配置先が:

https://example.com/docs/

だった場合、ブラウザは /img/cover.png をこう解釈します。

https://example.com/img/cover.png

こうではありません。

https://example.com/docs/img/cover.png

つまり、あなたの頭の中では「サイト内の画像」でも、ブラウザにとっては「ドメイン直下の画像」です。

ブラウザは間違っていません。

あなたが少し期待しすぎただけです。

baseUrl は飾りではない

Docusaurus を使っているなら、この問題は baseUrl とだいたい仲良しです。

たとえば:

const config = {
url: 'https://example.com',
baseUrl: '/docs/',
};

これはサイトが /docs/ 配下にあるという意味です。

その状態でこんなふうにハードコードすると:

<img src="/img/cover.png" alt="cover" />;

フレームワークが持っているデプロイ前提の処理を、自分でわざわざ無視していることになります。

より安全なのは、フレームワークにパスを組ませることです。

import useBaseUrl from '@docusaurus/useBaseUrl';

export default function Hero() {
const imageUrl = useBaseUrl('/img/cover.png');
return <img src={imageUrl} alt="cover" />;
}

これなら / 配置でも /docs/ 配置でも比較的壊れにくいです。

もちろん、サイトが絶対にルート直下にしか置かれないと本気で分かっているなら、ハードコードでも動くでしょう。

ただ、多くのバグはこの一文から始まります。

デプロイ方法は今後も変わらないはず。

本番環境は、その楽観を潰すのが好きです。

画像だけでは済まない

最初に気づくのは画像消失かもしれません。

でも本当に面倒なのは、たいていこちらです。

  • script chunk のパスがずれる
  • lazy load されたアセットが古い位置を指す
  • font URL に正しいプレフィックスが付かない
  • manifest、favicon、OG image の参照先が間違う

すると画面はこうなります。

  • スタイルが半分だけ生きている
  • ボタンは押せるがアイコンが消える
  • トップページは平気なのに下層ページが解体済みの見た目になる

こういう症状はよく次のように誤診されます。

  • build が壊れた
  • キャッシュが残っている
  • 依存パッケージのバージョンが悪い

もちろんそれらもありえます。

でも最初にパスを見るのが一番安いです。

しかも当たる確率がかなり高い。

相対パスも平気で刺してくる

たとえばこう書いたとします。

<img src="img/cover.png" alt="cover" />

現在のページが:

https://example.com/posts/hello/

なら、ブラウザはこう解決する可能性があります。

https://example.com/posts/hello/img/cover.png

これは次とは別物です。

https://example.com/img/cover.png

その結果、とても面倒な症状になります。

  • トップページは正常
  • 一部の記事ページは正常
  • さらに深いページだけ壊れる

相対パスは壊れていません。

ただ仕様通りに、妙に正直に動いているだけです。

そしてあなたは、その仕様をさっきまで忘れていただけです。

もう一つの定番:画面遷移は平気なのに、リロードで死ぬ

これも古典です。

たとえばフロントエンドのルートがこうあるとします。

/docs/intro

トップページからクリックで入ると正常。

なぜならフロントエンドルーターが処理するからです。

でも URL を直接開いたり、リロードしたりすると、サーバーが 404 を返します。

これはフロントエンドルートが壊れているとは限りません。

たいていはサーバーが次のことを知らないだけです。

実ファイルに一致しないなら index.html を返し、その後はフロントエンドに任せるべき。

Nginx なら、たとえばこういう設定が必要になります。

location /docs/ {
try_files $uri $uri/ /docs/index.html;
}

このような fallback がないと、client-side routing のページは「直接開いたときだけ」死にます。

この手のバグが嫌らしいのは、こう見えるからです。

  • サイト内遷移では正常
  • ブックマーク、共有 URL、リロードでだけ失敗

つまりかなり長く隠れられます。

誰かが普通の人間らしい使い方をするまでは。

どう調べると早いか

こういう問題では、最初にやるべきことは build のやり直しではありません。

DevTools の Network を開いてください。

見るべきはここです。

  1. 実際にどの URL が 404 になっているか
  2. プレフィックスが足りないのか、余計なのか
  3. /... の root-relative path を誤用していないか
  4. 消えているのは HTML か、JS chunk か、CSS か、単なる画像か
  5. 下層ページのリロード失敗なら、サーバー側 fallback が足りないのではないか

画面だけ見て「なぜ消えた」と悩んでも、あまり前に進みません。

画面は事情説明をしてくれません。

404 になっている URL のほうが、ずっと口が軽いです。

事故りにくい確認順

私はだいたいこの順で見ます。

1. 実際の配置場所を確認する

サイトが本当にどこに載っているかを確認します。

  • /
  • /docs/
  • /product/site/
  • CDN の path prefix の後ろ

想像ではなく、実 URL を見てください。

2. フレームワーク設定を見る

Docusaurus なら:

  • url
  • baseUrl
  • trailingSlash

他のフレームワークでも似たものがあります。

  • base
  • assetPrefix
  • publicPath

名前は違っても、だいたい同じ種類の苦しみです。

3. ハードコードされたパスを探す

特にこういうものです。

src="/..."
href="/..."
url(/...)
fetch('/...')

必ずしも間違いではありません。

ただし疑う価値がかなりあります。

4. 下層ページへの直接アクセスを試す

トップページだけで満足しないでください。

たとえば:

https://example.com/docs/some/page

を直接開きます。

サイト内遷移だけ成功して直アクセスで失敗するなら、問題はページ内容よりもルーティングやサーバー設定にあることが多いです。

5. キャッシュを消してもう一度見る

特に次です。

  • service worker
  • CDN cache
  • HTML と hashed assets の不整合

修正自体は終わっているのに、キャッシュが古い間違いを必死に守っていることは普通にあります。

後で効く習慣

原則は単純です。

デプロイ先に柔軟性が必要なプロジェクトでは、変わらない前提でアセットパスをベタ書きしない。

可能ならフレームワークに処理させる。

無理ならロジックを一箇所に寄せる。

たとえばこうです。

import useBaseUrl from '@docusaurus/useBaseUrl';

export function useAsset(path: string) {
return useBaseUrl(path);
}

そして統一して使います。

const logo = useAsset('/img/logo.svg');

これで世界は平和になりません。

でも将来デプロイ位置が変わったとき、プロジェクト全体から "/img/..." を発掘する作業はかなり減ります。

最後に

「ローカルでは動くのに、デプロイ後に壊れる」という現象は、神秘的な本番限定バグであることは意外と少ないです。

単に本番環境が、あなたの前提をもう庇ってくれなくなっただけです。

そして数ある原因の中でも、パスの扱いは最初に疑う価値が高く、しかも安い確認項目です。

だから次にこうなったら:

  • トップページは平気なのに下層ページがおかしい
  • 一部の画像だけ消える
  • JS や CSS が production でだけ消える
  • クリック遷移はできるのに、リロードで 404

まずフレームワークを罵倒する前に、

ブラウザキャッシュ、Node のバージョン、月の満ち欠け、あるいは半分しか読んでいない bundler の更新履歴を疑う前に、

パスを見てください。

問題が深いとは限りません。

自信満々で、間違ったディレクトリに入っていただけかもしれません。

☕ 1杯のコーヒーが支えになります

AIやフルスタックの情報発信を続けるため、ご支援お願いします。

cta-button
AI・開発・運用まで一括対応 icon
ALL

AI・開発・運用まで一括対応

アイデアからリリースまで、技術面はまるごとお任せください。

対応内容
  • 技術相談 + 開発 + デプロイ
  • 継続サポート & 拡張

🚀 次のプロジェクト、始めましょう!

カスタム開発や長期支援をご希望の方は、ぜひご相談ください。