Quick fix
# One-liner: core nginx fix — fall back nonexistent paths to the SPA entry; expose static-asset 404s separately
# location /app/ { alias /path/to/dist/; try_files $uri $uri/ /app/index.html; }
# location /app/assets/ { alias /path/to/dist/assets/; try_files $uri =404; }
Adjust these two locations to your deploy directory, then nginx -t && nginx -s reload.
What you will run into
Local vite dev works fine; after building and deploying to a server, the homepage loads and in-site route navigation works, but manually refreshing a sub-route page (deep link) white-screens, and the deep-link request returns 404 in the console.
Never reproducible locally; the problem only shows after deploy.
Why this happens
With vue-router using createWebHistory (HTML5 History mode), URLs carry no #. On refresh, the browser bypasses the front-end router entirely and requests the sub-route path directly from the server — for example /app/docs/setup.
The server looks for a physical file at that path, does not find one, and returns 404, which renders as a white screen.
In-app navigation never hits the server, because the router intercepts it. That is why "navigation works, but refresh white-screens" is the strongest signature of this problem.
| Scenario | Behavior |
|---|---|
| In-site navigation | ✅ Frontend router handles it; no server request |
| Refresh a deep link | ❌ Browser directly requests /app/docs/setup; server has no such file → 404 |
| Direct homepage access | ✅ Physical file exists |
vite dev has a built-in history fallback (every path falls back to index.html), so local dev always works and the bug appears only after deploy — the classic "dev and production static-server behavior differ" case.
Deterministic takeaway: the production server must fall back unknown paths to index.html — nginx try_files $uri $uri/ /index.html;, the SPA fallback option on static hosting platforms, or a Node server returning index.html for app.get('*', ...).
The fix is verified when refreshing a deep link returns 200 instead of 404.
Step by step
1. First diagnostic step: base consistency — Diagnose
vite.config's base, createWebHistory(base), and the server deploy directory must be exactly consistent (especially the trailing /). Partial asset 404s / missing icons are usually base inconsistency.
2. nginx history fallback (core fix) — Required
location /app/ {
alias /path/to/dist/;
try_files $uri $uri/ /app/index.html;
}
3. Static-asset 404s must not be swallowed by the fallback — Required
A blanket fallback returns 200 + HTML even for missing JS/CSS (harder to debug when dynamic imports fail; cross-confirms with ChunkLoadError). Give assets their own location:
location /app/assets/ {
alias /path/to/dist/assets/;
try_files $uri =404;
}
4. Apache equivalent — Required
.htaccess: RewriteEngine On + rewrite to /app/index.html when file/dir doesn't exist (same logic as step 2).
How to confirm it works
curl assertions for the three path classes (expected output):
curl -I http://your-domain/app/docs/setup
→ HTTP 200 (deep link falls back to the HTML entry)
curl -I http://your-domain/app/assets/main.js
→ HTTP 200 (real asset)
curl -I http://your-domain/app/assets/missing.js
→ HTTP 404 (missing asset correctly exposed, not swallowed)
Refreshing any deep link no longer white-screens (measured: after the fix, deep link 200, real asset 200, missing asset 404).
Other environments
| Environment | Notes |
|---|---|
| nginx alias deploy | Deep-link fallback + assets own location (measured fix) |
| nginx root deploy | Same try_files $uri $uri/ /index.html; mind the base path |
| Apache | .htaccess rewrite; same logic |
| base inconsistency | Partial asset 404 / missing icons; check base before fallback |
| Blanket fallback | Missing JS also returns 200 HTML; harder to debug (measured pitfall) |
Symptom
Local vite dev works fine; after build & deploy, the homepage and in-app routing work, but manually refreshing a deep-linked sub-route goes blank — the network panel shows the deep link returning 404.
Cause
vue-router uses createWebHistory (HTML5 History mode), so URLs have no #. On refresh the browser requests the sub-route path (e.g. /app/docs/setup) directly from the server; the server has no such physical file and returns 404 → blank page. vite dev ships its own history fallback, so this never reproduces locally — it only shows up after deployment.
Solution
- base consistency (check first):
baseinvite.config,createWebHistory(base), and the server deployment directory must match exactly (watch the trailing/). Partial asset 404s / missing icons are usually a base mismatch. - nginx history fallback (core fix): fall back non-existent paths to the SPA entry:
location /app/ {
alias /path/to/dist/;
try_files $uri $uri/ /app/index.html;
}
- Don't let the fallback swallow real static 404s (measured fix): a blanket fallback turns missing JS/CSS into 200 + HTML, which makes dynamic-import failures much harder to debug (cross-check the ChunkLoadError entry). Give assets their own location so real 404s stay visible:
location /app/assets/ {
alias /path/to/dist/assets/;
try_files $uri =404;
}
- Apache equivalent:
RewriteEngine On+ rewrite to/app/index.htmlwhen the file/directory doesn't exist (.htaccess, same logic as 2).
Verification Evidence
- Local test (2026-08-29): nginx 1.28.3 + minimal SPA fixture (
index.html+assets/),aliasdeploy +curlassertions. - Repro: without fallback
GET /app/docs/setup→ 404 (blank-refresh root cause confirmed),/app/index.html→ 200. - Article fix: with
try_files ... /app/index.html, deep link → 200 (HTML entry); butGET /app/assets/missing.js→ 200 HTML — static 404 swallowed (a pitfall the article doesn't mention). - After the fix: deep link 200, real assets 200, missing assets 404 (properly exposed).
local repro log 2026-08-29
Applicable tools (2)
This problem is linked from the tool pages below — reachable from either side.
vite-deploynginxBundled solutions (combos)
This problem is part of the following verified bundles.
- Combo Draft: Web Deploy Verification verified
Source & evidence
← Build & Deploy domain | Problem Catalog Experience | Facing another issue? Go to the Helpdesk