diff --git a/blog/2025/06-18-works-locally-breaks-after-deploy/index.md b/blog/2025/06-18-works-locally-breaks-after-deploy/index.md new file mode 100644 index 00000000000..65a2204a5aa --- /dev/null +++ b/blog/2025/06-18-works-locally-breaks-after-deploy/index.md @@ -0,0 +1,422 @@ +--- +slug: works-locally-breaks-after-deploy +title: 本機正常,上線就壞掉?多半不是鬼,是路徑 +authors: Z. Yuan +image: /img/2025/0618-static-site-baseurl-trap.svg +tags: [docusaurus, deployment, frontend, debugging] +description: 本機正常、部署後 404,通常不是前端在鬧脾氣,而是你的 baseUrl、資源路徑、快取和部署假設一起聯手教育你。 +--- + +有一種 bug 很常見,而且很沒創意。 + +你在本機開發時看起來一切正常: + +- 頁面會開 +- 圖片會顯示 +- CSS 沒炸 +- 連結也點得動 + +然後一部署上去,整站開始表演: + +- 圖片 404 +- JS chunk 載不到 +- CSS 路徑錯掉 +- 子頁正常,重新整理就死 +- `/docs/intro` 可以點進去,但直接開就變成伺服器一臉無辜 + +這種時候,很多人第一反應是: + +> 奇怪,明明本機是好的。 + +對。 + +本機通常都很好。 + +因為本機最會包庇你。 + + + +真正的問題通常不是「程式昨天還可以,今天突然不行」。 + +而是你把一堆**只在本機成立的假設**,帶去了不打算配合你的正式環境。 + +其中最常見的一類,就是: + +> **路徑。** + +講白一點:不是鬼,是你把檔案放在 A,卻叫瀏覽器去 B 找。 + +## 這類問題為什麼特別常見? + +因為本機開發環境通常很寬容: + +- dev server 會幫你補路由 +- 網站常常掛在根路徑 `/` +- 沒有 CDN、反向代理、子目錄部署 +- 快取比較少,錯誤比較不持久 +- 你習慣用點進去,不習慣直接打 URL 測 + +所以很多錯誤在本機根本不容易暴露。 + +一旦到正式環境,條件稍微變一下,問題就出來了。 + +例如: + +- 站不是掛在 `/`,而是掛在 `/blog/` 或 `/docs/` +- 靜態資源有 CDN 前綴 +- 伺服器不幫你做 SPA fallback +- 反向代理把前綴路徑吃掉或重寫錯 +- 你以為是「絕對路徑」,其實只是「相對於網域根目錄」 + +這幾件事一湊在一起,整站就會用 404 跟你溝通。 + +很直接。 + +## 最典型的坑:你以為 `/img/a.png` 很安全 + +先看這種寫法: + +```md +![cover](/img/cover.png) +``` + +或這種: + +```tsx +cover +``` + +如果你的網站部署在: + +```text +https://example.com/ +``` + +那這通常沒事。 + +但如果你的網站其實部署在: + +```text +https://example.com/docs/ +``` + +那瀏覽器看到 `/img/cover.png`,會去找: + +```text +https://example.com/img/cover.png +``` + +不是: + +```text +https://example.com/docs/img/cover.png +``` + +也就是說,你心裡想的是「站內圖片」,瀏覽器理解的是「網域根目錄圖片」。 + +它沒有錯。 + +是你想太多。 + +## `baseUrl` 不是裝飾品 + +如果你用的是 Docusaurus,這個問題通常跟 `baseUrl` 脫不了關係。 + +例如: + +```ts +const config = { + url: 'https://example.com', + baseUrl: '/docs/', +}; +``` + +這代表網站實際掛在 `/docs/` 底下。 + +此時如果你自己手寫字串路徑: + +```tsx +cover +``` + +你其實是在**繞過框架的部署設定**。 + +比較穩妥的方式通常是交給框架處理,例如: + +```tsx +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export default function Hero() { + const imageUrl = useBaseUrl('/img/cover.png'); + return cover; +} +``` + +這樣在 `/` 跟 `/docs/` 下面都比較不容易出事。 + +如果你明知道網站只會掛在根目錄,那硬寫也不是不行。 + +但很多 bug 的根源就是: + +> 一開始只打算放根目錄,後來部署方式變了,字串路徑還停留在美好舊時代。 + +程式沒有懷舊能力,所以它會直接壞給你看。 + +## 不只圖片,JS、CSS、字型也會一起陪葬 + +很多人第一次遇到時,只注意到圖片不見。 + +其實更麻煩的通常是這些: + +- script chunk 路徑錯 +- lazy load 的 asset 指到舊位置 +- font URL 沒帶對前綴 +- manifest、favicon、social image 指錯 + +然後畫面看起來就像: + +- 樣式一半有、一半沒有 +- 按鈕能按,但 icons 消失 +- 首頁正常,內頁像被拆過 + +這種現象很容易讓人誤判成: + +- 打包壞了 +- 快取沒清 +- 某個套件版本爆了 + +當然,這些也可能是真的。 + +但先檢查路徑,成本最低。 + +因為它出事的機率高得很不體面。 + +## 另一個老坑:相對路徑在巢狀頁面很會背刺你 + +例如你寫: + +```html +cover +``` + +如果目前頁面在: + +```text +https://example.com/posts/hello/ +``` + +瀏覽器可能會把它解成: + +```text +https://example.com/posts/hello/img/cover.png +``` + +這跟你以為的: + +```text +https://example.com/img/cover.png +``` + +完全不是同一回事。 + +所以你會看到一種很煩的症狀: + +- 首頁正常 +- 某些文章頁正常 +- 深一層的頁面全部掛掉 + +因為相對路徑不是壞掉。 + +它只是非常誠實地照規則做事。 + +而你沒有真的記得那個規則。 + +## 還有一種:前端路由正常,直接刷新就死 + +這也是經典。 + +例如你有一個 SPA 或靜態站前端路由: + +```text +/docs/intro +``` + +從首頁點進去時正常。 + +因為是前端接手導頁。 + +但你直接開這個 URL,或重新整理頁面,伺服器就回你 404。 + +原因通常不是前端路由壞掉。 + +而是伺服器根本不知道: + +> 這個路徑其實應該回 `index.html`,再交給前端處理。 + +像 Nginx 常見會需要這種設定: + +```nginx +location /docs/ { + try_files $uri $uri/ /docs/index.html; +} +``` + +如果沒有類似 fallback,很多 client-side route 都會在「直接進入」時死亡。 + +這種 bug 也很會騙人,因為: + +- 用站內連結進去時正常 +- 用書籤、分享連結、重新整理時才壞 + +於是它可以潛伏很久。 + +直到有人真的照正常人方式使用網站。 + +## 怎麼查比較快?先看 Network,不要先拜神 + +碰到這種問題,第一件事不是重跑 build。 + +第一件事是打開瀏覽器 DevTools 的 **Network**。 + +看這幾件事: + +1. **哪個 URL 真的 404?** +2. **它是少了前綴,還是多了前綴?** +3. **是根目錄 `/` 被誤用,還是相對路徑跑偏?** +4. **是 HTML 找不到,還是 JS chunk 找不到?** +5. **重新整理內頁時,是不是伺服器沒做 fallback?** + +很多人 debug 卡很久,是因為只盯著畫面說「怎麼沒出來」。 + +畫面不會告訴你答案。 + +404 URL 會。 + +差很多。 + +## 一套比較不容易出事的檢查順序 + +我通常會這樣查: + +### 1. 先確認實際部署位置 + +先搞清楚網站到底掛在哪: + +- `/` +- `/docs/` +- `/product/site/` +- CDN path prefix 後面 + +不要用想像的。 + +看實際網址。 + +### 2. 檢查框架設定 + +例如 Docusaurus: + +- `url` +- `baseUrl` +- `trailingSlash` + +例如 Vite / Next / 其他框架,也會有自己的: + +- `base` +- `assetPrefix` +- `publicPath` + +名字不同,問題差不多。 + +### 3. 找出所有硬編碼路徑 + +特別是這些: + +```text +/src="/..." +/href="/..." +url(/...) +fetch('/...') +``` + +它們不一定錯。 + +但很值得懷疑。 + +### 4. 測「直接進內頁」 + +不要只測首頁。 + +請直接開: + +```text +https://example.com/docs/some/page +``` + +如果只有站內跳轉能用,那問題通常不在「頁面內容」,而在路由或伺服器設定。 + +### 5. 清掉快取再看一次 + +尤其是: + +- service worker +- CDN cache +- hashed assets 與 HTML 不同步 + +有時候你修好了,但快取還在堅持它的舊世界觀。 + +也很常見。 + +## 一個實際上比較安全的習慣 + +原則其實不複雜: + +> **不要在需要部署彈性的專案裡,到處手寫你自以為不會變的資源路徑。** + +能交給框架處理,就交給框架。 + +能集中封裝,就不要散落全專案。 + +例如包一層: + +```ts +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export function useAsset(path: string) { + return useBaseUrl(path); +} +``` + +之後統一用: + +```tsx +const logo = useAsset('/img/logo.svg'); +``` + +這不會讓世界和平。 + +但至少未來部署位置改掉時,你不需要在專案裡挖一百個 `"/img/..."`。 + +## 最後 + +「本機正常、上線壞掉」這件事,很多時候不是神祕 bug。 + +只是正式環境終於停止縱容你。 + +而在所有原因裡,**路徑問題**通常是最常見、最便宜、也最值得先懷疑的那一個。 + +所以如果你下次看到: + +- 首頁正常,內頁怪怪的 +- 部分圖片消失 +- JS/CSS 在 production 失蹤 +- 點進去正常,重新整理就 404 + +先不要急著怪框架。 + +也先不要把責任推給瀏覽器快取、Node 版本、月亮位置、或某個你其實沒看懂的 bundler 更新。 + +先去看路徑。 + +很多時候,問題沒有很深。 + +只是你剛好走錯目錄而已。 diff --git a/i18n/en/docusaurus-plugin-content-blog/2025/06-18-works-locally-breaks-after-deploy/index.md b/i18n/en/docusaurus-plugin-content-blog/2025/06-18-works-locally-breaks-after-deploy/index.md new file mode 100644 index 00000000000..80b9cc6259d --- /dev/null +++ b/i18n/en/docusaurus-plugin-content-blog/2025/06-18-works-locally-breaks-after-deploy/index.md @@ -0,0 +1,410 @@ +--- +slug: works-locally-breaks-after-deploy +title: Works Locally, Breaks After Deploy? It’s Usually the Path +authors: Z. Yuan +image: /img/2025/0618-static-site-baseurl-trap.svg +tags: [docusaurus, deployment, frontend, debugging] +description: If your site works locally and breaks in production, it usually isn't dark magic. It's your baseUrl, asset paths, cache, and deployment assumptions forming a small but effective conspiracy. +--- + +There is a category of bug that is both common and aggressively unoriginal. + +Everything looks fine in local development: + +- the page loads +- the image shows up +- the CSS survives +- the links still pretend to be useful + +Then you deploy, and the site starts performing modern interpretive dance: + +- images return 404 +- JS chunks fail to load +- CSS paths go missing +- subpages work until refresh, then die +- `/docs/intro` works when clicked, but opening it directly makes the server act surprised + +At that point, the usual reaction is: + +> But it worked locally. + +Yes. + +Local environments are often excellent at lying to you. + + + +The real issue usually isn't that the code randomly broke overnight. + +It's that you carried a set of **assumptions that only held locally** into a production environment that had no intention of cooperating. + +One of the most common versions of that problem is this: + +> **Paths.** + +In plainer terms: the file is in one place, and you told the browser to look somewhere else. + +## Why this happens so often + +Because local development is usually forgiving: + +- the dev server patches over routing mistakes +- the site often runs at the root path `/` +- there is no CDN, reverse proxy, or subdirectory deployment +- caching is lighter, so mistakes don't linger as long +- people click around locally, but rarely test direct deep links + +So a lot of path-related errors never fully surface until production. + +Then the environment changes slightly, and the site responds with a 404 and no emotional support. + +Typical changes include: + +- the site is hosted under `/blog/` or `/docs/` instead of `/` +- static assets get a CDN prefix +- the server does not provide SPA fallback +- a reverse proxy strips or rewrites the path prefix incorrectly +- what you thought was an “absolute path” was really just “absolute from the domain root” + +That is usually enough. + +## The classic trap: `/img/a.png` + +Consider this Markdown: + +```md +![cover](/img/cover.png) +``` + +Or this JSX: + +```tsx +cover +``` + +If your site is deployed at: + +```text +https://example.com/ +``` + +this is usually fine. + +But if the real deployment target is: + +```text +https://example.com/docs/ +``` + +then the browser interprets `/img/cover.png` as: + +```text +https://example.com/img/cover.png +``` + +not: + +```text +https://example.com/docs/img/cover.png +``` + +So in your head, it was “an image inside my site.” + +In the browser's head, it was “an image at the domain root.” + +The browser is not confused. + +It is merely less imaginative than you. + +## `baseUrl` is not decorative + +If you are using Docusaurus, this problem is often tied to `baseUrl`. + +For example: + +```ts +const config = { + url: 'https://example.com', + baseUrl: '/docs/', +}; +``` + +That means the site actually lives under `/docs/`. + +If you then hardcode this: + +```tsx +cover +``` + +you are bypassing the framework's deployment-aware path handling. + +A safer approach is to let the framework build the path: + +```tsx +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export default function Hero() { + const imageUrl = useBaseUrl('/img/cover.png'); + return cover; +} +``` + +That tends to survive both `/` and `/docs/` deployments. + +If you truly know the site will only ever live at the root, hardcoding may be acceptable. + +But a lot of bugs begin with: + +> We thought deployment would never change. + +Production enjoys disproving that sentence. + +## It is not just images + +People often notice missing images first. + +The more annoying breakages are usually these: + +- script chunk paths are wrong +- lazy-loaded assets point to stale locations +- font URLs miss the correct prefix +- manifest, favicon, or social image paths point to the wrong place + +The result is the kind of page that looks half-built and slightly offended: + +- some styles load, some don't +- buttons work, icons vanish +- the homepage is fine, inner pages look dismantled + +This is why path bugs are often misdiagnosed as: + +- broken builds +- stale caches +- package version regressions + +Those things can happen too. + +But checking the path is usually the cheapest first move. + +And the probability is embarrassingly high. + +## Relative paths are also happy to betray you + +Suppose you write: + +```html +cover +``` + +If the current page is: + +```text +https://example.com/posts/hello/ +``` + +then the browser may resolve it as: + +```text +https://example.com/posts/hello/img/cover.png +``` + +which is not the same as: + +```text +https://example.com/img/cover.png +``` + +That produces a very annoying class of symptoms: + +- homepage works +- some article pages work +- deeper pages fail + +The relative path is not malfunctioning. + +It is following the rules exactly. + +You just were not thinking about those rules five minutes ago. + +## Another old favorite: navigation works, refresh dies + +This one is also common. + +You have a frontend route like: + +```text +/docs/intro +``` + +Clicking into it from the homepage works. + +Because the frontend router takes over. + +But opening that URL directly, or refreshing the page, returns a 404 from the server. + +That usually does not mean the frontend route is broken. + +It means the server was never told: + +> When this path does not match a real file, serve `index.html` and let the frontend router decide. + +For Nginx, that often looks like this: + +```nginx +location /docs/ { + try_files $uri $uri/ /docs/index.html; +} +``` + +Without a fallback like that, many client-side routes fail only when directly requested. + +Which makes the bug wonderfully deceptive: + +- internal navigation works +- bookmarks, shared links, and refreshes fail + +So it can hide for a long time. + +Until someone uses the site like a normal person. + +## How to debug this faster + +Open DevTools and go to **Network**. + +Before rebuilding everything, check these: + +1. **Which URL actually returned 404?** +2. **Is it missing a prefix, or does it have an extra one?** +3. **Did root-relative `/...` get used where a deployment-aware path was needed?** +4. **Is the missing resource HTML, a JS chunk, CSS, or an asset?** +5. **When an inner page fails on refresh, is the server missing a fallback rule?** + +A lot of debugging time gets wasted staring at the page and saying, “why is it gone?” + +The page will not explain itself. + +The failing URL usually will. + +## A practical checklist + +This is the order I usually use. + +### 1. Confirm the real deployment location + +Figure out where the site actually lives: + +- `/` +- `/docs/` +- `/product/site/` +- behind a CDN path prefix + +Use the real URL, not the one still living in your memory. + +### 2. Check framework config + +For Docusaurus, look at: + +- `url` +- `baseUrl` +- `trailingSlash` + +Other frameworks have their equivalents: + +- `base` +- `assetPrefix` +- `publicPath` + +Different names, same category of pain. + +### 3. Find hardcoded paths + +Especially these patterns: + +```text +src="/..." +href="/..." +url(/...) +fetch('/...') +``` + +They are not always wrong. + +They are simply suspicious often enough to deserve attention. + +### 4. Test direct entry to inner pages + +Do not stop at the homepage. + +Open something like: + +```text +https://example.com/docs/some/page +``` + +If internal navigation works but direct entry fails, the problem is usually routing or server configuration, not page content. + +### 5. Clear caches and test again + +Especially: + +- service workers +- CDN cache +- mismatched HTML and hashed assets + +Sometimes you fixed the problem and the cache is just committed to preserving the old mistake. + +## A habit that saves time later + +The principle is simple: + +> **If a project may be deployed under different prefixes, stop hardcoding asset paths like they are eternal truths.** + +Let the framework handle them when possible. + +If not, centralize the logic. + +For example: + +```ts +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export function useAsset(path: string) { + return useBaseUrl(path); +} +``` + +Then use it consistently: + +```tsx +const logo = useAsset('/img/logo.svg'); +``` + +This will not solve every deployment bug. + +But it does reduce the number of future archaeological digs through `"/img/..."` strings. + +## Final + +“Works locally, breaks after deploy” is often not a mysterious production-only bug. + +It is just production finally refusing to cover for your assumptions. + +And among all the possible causes, **path handling** is one of the cheapest and most profitable things to suspect first. + +So the next time you see this pattern: + +- homepage works, inner pages act strange +- some images vanish +- JS or CSS disappears only in production +- clicking works, refreshing gives 404 + +Do not start by blaming the framework. + +Do not immediately accuse browser cache, Node version, the moon phase, or some bundler release note you only half read. + +Check the path first. + +A lot of the time, the bug is not deep. + +You just walked into the wrong directory with confidence. diff --git a/i18n/ja/docusaurus-plugin-content-blog/2025/06-18-works-locally-breaks-after-deploy/index.md b/i18n/ja/docusaurus-plugin-content-blog/2025/06-18-works-locally-breaks-after-deploy/index.md new file mode 100644 index 00000000000..e3a27ad3c64 --- /dev/null +++ b/i18n/ja/docusaurus-plugin-content-blog/2025/06-18-works-locally-breaks-after-deploy/index.md @@ -0,0 +1,412 @@ +--- +slug: works-locally-breaks-after-deploy +title: ローカルでは動くのに、デプロイ後に壊れる? たいてい原因はパスです +authors: Z. Yuan +image: /img/2025/0618-static-site-baseurl-trap.svg +tags: [docusaurus, deployment, frontend, debugging] +description: ローカルでは正常なのに本番で壊れるなら、だいたいは闇の力ではありません。baseUrl、アセットパス、キャッシュ、そして雑なデプロイ前提が静かに噛み合っているだけです。 +--- + +よくあるのに、まったく面白くないバグがあります。 + +ローカル開発では全部まともに見えるのです。 + +- ページは開く +- 画像も出る +- CSS も生きている +- リンクも一応仕事をしている + +ところがデプロイした瞬間、サイトが急に芸を始めます。 + +- 画像が 404 +- JS chunk が読めない +- CSS のパスが消える +- 下層ページは開けるのに、リロードすると死ぬ +- `/docs/intro` はクリックすれば開くのに、直接開くとサーバーが知らないふりをする + +このとき多くの人はこう言います。 + +> でもローカルでは動いていました。 + +ええ。 + +ローカル環境は、あなたのミスを甘やかすのが得意です。 + + + +本当の問題は「コードが突然気分で壊れた」ことではありません。 + +たいていは、**ローカルでしか成立しない前提**を、そのまま本番に持ち込んだだけです。 + +その中でも特に多いのがこれです。 + +> **パス。** + +もっと雑に言うと、ファイルは A にあるのに、ブラウザには B を見に行かせている、という話です。 + +## なぜこんなに起こるのか + +ローカル開発環境はだいたい寛容だからです。 + +- dev server がルーティングのミスを雑に吸収する +- サイトがルート `/` 配下で動いていることが多い +- CDN もリバースプロキシもサブディレクトリ配置もない +- キャッシュが軽いので、ミスが長生きしにくい +- ローカルでは深い URL を直打ちせず、だいたいクリックで移動する + +だからパスまわりの問題は、本番に出るまで露出しないことが多いのです。 + +本番で少し条件が変わると、サイトは無言で 404 を返してきます。 + +よくある変化はこんなものです。 + +- サイトが `/` ではなく `/blog/` や `/docs/` の下に載る +- 静的アセットに CDN プレフィックスが付く +- サーバーが SPA fallback をしてくれない +- リバースプロキシが前置パスを削る、または間違って書き換える +- 「絶対パス」のつもりが、実際には「ドメインルート基準のパス」だった + +これだけで十分壊れます。 + +## 典型的な罠:`/img/a.png` + +まずはこういう書き方です。 + +```md +![cover](/img/cover.png) +``` + +あるいは JSX でこう。 + +```tsx +cover +``` + +サイトが次の場所にあるなら: + +```text +https://example.com/ +``` + +たいてい問題ありません。 + +でも実際の配置先が: + +```text +https://example.com/docs/ +``` + +だった場合、ブラウザは `/img/cover.png` をこう解釈します。 + +```text +https://example.com/img/cover.png +``` + +こうではありません。 + +```text +https://example.com/docs/img/cover.png +``` + +つまり、あなたの頭の中では「サイト内の画像」でも、ブラウザにとっては「ドメイン直下の画像」です。 + +ブラウザは間違っていません。 + +あなたが少し期待しすぎただけです。 + +## `baseUrl` は飾りではない + +Docusaurus を使っているなら、この問題は `baseUrl` とだいたい仲良しです。 + +たとえば: + +```ts +const config = { + url: 'https://example.com', + baseUrl: '/docs/', +}; +``` + +これはサイトが `/docs/` 配下にあるという意味です。 + +その状態でこんなふうにハードコードすると: + +```tsx +cover; +``` + +フレームワークが持っているデプロイ前提の処理を、自分でわざわざ無視していることになります。 + +より安全なのは、フレームワークにパスを組ませることです。 + +```tsx +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export default function Hero() { + const imageUrl = useBaseUrl('/img/cover.png'); + return cover; +} +``` + +これなら `/` 配置でも `/docs/` 配置でも比較的壊れにくいです。 + +もちろん、サイトが絶対にルート直下にしか置かれないと本気で分かっているなら、ハードコードでも動くでしょう。 + +ただ、多くのバグはこの一文から始まります。 + +> デプロイ方法は今後も変わらないはず。 + +本番環境は、その楽観を潰すのが好きです。 + +## 画像だけでは済まない + +最初に気づくのは画像消失かもしれません。 + +でも本当に面倒なのは、たいていこちらです。 + +- script chunk のパスがずれる +- lazy load されたアセットが古い位置を指す +- font URL に正しいプレフィックスが付かない +- manifest、favicon、OG image の参照先が間違う + +すると画面はこうなります。 + +- スタイルが半分だけ生きている +- ボタンは押せるがアイコンが消える +- トップページは平気なのに下層ページが解体済みの見た目になる + +こういう症状はよく次のように誤診されます。 + +- build が壊れた +- キャッシュが残っている +- 依存パッケージのバージョンが悪い + +もちろんそれらもありえます。 + +でも最初にパスを見るのが一番安いです。 + +しかも当たる確率がかなり高い。 + +## 相対パスも平気で刺してくる + +たとえばこう書いたとします。 + +```html +cover +``` + +現在のページが: + +```text +https://example.com/posts/hello/ +``` + +なら、ブラウザはこう解決する可能性があります。 + +```text +https://example.com/posts/hello/img/cover.png +``` + +これは次とは別物です。 + +```text +https://example.com/img/cover.png +``` + +その結果、とても面倒な症状になります。 + +- トップページは正常 +- 一部の記事ページは正常 +- さらに深いページだけ壊れる + +相対パスは壊れていません。 + +ただ仕様通りに、妙に正直に動いているだけです。 + +そしてあなたは、その仕様をさっきまで忘れていただけです。 + +## もう一つの定番:画面遷移は平気なのに、リロードで死ぬ + +これも古典です。 + +たとえばフロントエンドのルートがこうあるとします。 + +```text +/docs/intro +``` + +トップページからクリックで入ると正常。 + +なぜならフロントエンドルーターが処理するからです。 + +でも URL を直接開いたり、リロードしたりすると、サーバーが 404 を返します。 + +これはフロントエンドルートが壊れているとは限りません。 + +たいていはサーバーが次のことを知らないだけです。 + +> 実ファイルに一致しないなら `index.html` を返し、その後はフロントエンドに任せるべき。 + +Nginx なら、たとえばこういう設定が必要になります。 + +```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. ハードコードされたパスを探す + +特にこういうものです。 + +```text +src="/..." +href="/..." +url(/...) +fetch('/...') +``` + +必ずしも間違いではありません。 + +ただし疑う価値がかなりあります。 + +### 4. 下層ページへの直接アクセスを試す + +トップページだけで満足しないでください。 + +たとえば: + +```text +https://example.com/docs/some/page +``` + +を直接開きます。 + +サイト内遷移だけ成功して直アクセスで失敗するなら、問題はページ内容よりもルーティングやサーバー設定にあることが多いです。 + +### 5. キャッシュを消してもう一度見る + +特に次です。 + +- service worker +- CDN cache +- HTML と hashed assets の不整合 + +修正自体は終わっているのに、キャッシュが古い間違いを必死に守っていることは普通にあります。 + +## 後で効く習慣 + +原則は単純です。 + +> **デプロイ先に柔軟性が必要なプロジェクトでは、変わらない前提でアセットパスをベタ書きしない。** + +可能ならフレームワークに処理させる。 + +無理ならロジックを一箇所に寄せる。 + +たとえばこうです。 + +```ts +import useBaseUrl from '@docusaurus/useBaseUrl'; + +export function useAsset(path: string) { + return useBaseUrl(path); +} +``` + +そして統一して使います。 + +```tsx +const logo = useAsset('/img/logo.svg'); +``` + +これで世界は平和になりません。 + +でも将来デプロイ位置が変わったとき、プロジェクト全体から `"/img/..."` を発掘する作業はかなり減ります。 + +## 最後に + +「ローカルでは動くのに、デプロイ後に壊れる」という現象は、神秘的な本番限定バグであることは意外と少ないです。 + +単に本番環境が、あなたの前提をもう庇ってくれなくなっただけです。 + +そして数ある原因の中でも、**パスの扱い**は最初に疑う価値が高く、しかも安い確認項目です。 + +だから次にこうなったら: + +- トップページは平気なのに下層ページがおかしい +- 一部の画像だけ消える +- JS や CSS が production でだけ消える +- クリック遷移はできるのに、リロードで 404 + +まずフレームワークを罵倒する前に、 + +ブラウザキャッシュ、Node のバージョン、月の満ち欠け、あるいは半分しか読んでいない bundler の更新履歴を疑う前に、 + +パスを見てください。 + +問題が深いとは限りません。 + +自信満々で、間違ったディレクトリに入っていただけかもしれません。 diff --git a/static/img/2025/0618-static-site-baseurl-trap.svg b/static/img/2025/0618-static-site-baseurl-trap.svg new file mode 100644 index 00000000000..3967a0df84a --- /dev/null +++ b/static/img/2025/0618-static-site-baseurl-trap.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + Local + Production + /img/cover.png + 404 /img/cover.png + needs /docs/img/cover.png + + + Works locally. Breaks after deploy. + Usually not dark magic. Usually your paths. + + baseUrl / asset path + \ No newline at end of file