Client-side routing only works once the client is running. Press F5 on example.com/products/42, or paste that link into a fresh tab, and the browser asks the server for /products/42 — a path that exists in your route table and nowhere on disk. A static host answers 404, and the bug looks bizarre because in-app navigation to that same page works perfectly.
The fix is a history-API fallback: for any request that does not match a real file, serve index.html and let the router sort it out. Vite 25,978 's dev server does this automatically, so the failure first shows up on deploy.
location / {
try_files $uri $uri/ /index.html;
}
location /assets/ {
try_files $uri =404; # hashed bundles: 404 instead of silently serving HTML
}Every other host says the same thing in its own dialect. In Express 24,430 , mount express.static('dist') first and end with app.get('*splat', (req, res) => res.sendFile(path.resolve('dist/index.html'))). Apache needs a .htaccess with RewriteCond %{REQUEST_FILENAME} !-f and RewriteRule ^ index.html [L]; Netlify 4,096 a _redirects file containing /* /index.html 200; Vercel 3,061 a rewrites entry in vercel.json; S3 behind CloudFront 24 a custom error response mapping 403 and 404 to /index.html with status 200.
The second location block is the part people forget. A blanket fallback also catches typos in bundle URLs, so a missing /assets/index-a1b2.js is served as HTML and the browser reports "Unexpected token '<'" from a JavaScript file. Exclude the asset directory and the API prefix, and keep the fallback uncached: no-cache on index.html, immutable caching on the hashed assets.