Why does my page show 404 after refresh — when links inside the site work fine

The symptom
You've deployed your app. The homepage loads. You click "About" — the address changes to /about, the page is there. Everything works.
You press F5 — 404 Not Found. Or you send the /about link to a friend and they get an error. Even though that same page was on your screen five seconds ago.
And locally everything was perfect — you could refresh as much as you liked.
Here's what's going on. Inside the app, JavaScript handles the move to /about; the server knows nothing about it. On refresh, the browser honestly asks the server for the /about file. The server has no such file — it only has index.html and a folder of scripts. The author of a popular workaround for GitHub Pages puts it in one sentence: the server returns 404 because it knows nothing of /foo.
We covered the mechanics in detail in what is client-side routing. Here — just how to fix it.
Why it worked locally
This is the first thing that throws people off. The Vite dev server runs in appType: 'spa' mode by default: the docs say it includes an SPA fallback — for any unknown path it serves index.html. vite preview does the same.
So locally, it's all configured for you. A regular host behaves like a plain file server. It's a special case of the broader story of why an app works locally but breaks in production.
Cause 1. The host wasn't told to serve index.html for every address
The most common cause — almost always this one.
How to check. Open an inner address directly in an incognito window. Or in a terminal:
curl -I https://your-site.app/about
If the first line of the response says 404 and your build folder (dist) has no about/index.html file — this is it.
How to fix. You need a rule that "rewrites any path to /index.html" while the address in the browser stays the same. Every host writes it differently.
-
Netlify. A
_redirectsfile in the published folder (for Vite, put it inpublic/):/* /index.html 200The
200code turns the redirect into a rewrite: the address stays as it was. Per Netlify's docs, this rule doesn't shadow files that actually exist — scripts and images are still served as is. -
Vercel. A
vercel.jsonfile in the project root, straight from their Vite docs:{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } -
Cloudflare Pages. Nothing to write. If your build root has no
404.htmlfile, Pages treats the project as an SPA and serves the app for any path. The trap runs the other way: add your own404.htmland SPA mode switches off. -
Your own nginx server.
location / { try_files $uri $uri/ /index.html; }Nginx checks files in order. If none is found, per the docs it does an internal redirect to the last parameter — our
index.html.
If you're still choosing where to deploy, Vercel vs. Netlify will help.
Cause 2. The host can't rewrite addresses at all
The main example is GitHub Pages. It has no rewrite rules; files are served exactly as they sit.
How to check. Your site is on username.github.io, and none of the config files above help.
How to fix. Three options, from simple to proper:
- Hash routing. Addresses become
/#/about. Everything after#is never sent to the server, so refresh doesn't break. The downside is ugly addresses. React Router has a separate mode for this. - The 404.html trick. The
spa-github-pagesproject ships a special404.htmlwhose script turns the path into a query parameter and redirects to the homepage, where the app restores the address. It works, but it's a patch. - Move to a host with rewrites — Netlify, Vercel or Cloudflare Pages solve this for free in one line.
Cause 3. The site lives in a subfolder
A similar symptom, but trickier. On username.github.io/my-app/ the homepage loads, but inner addresses give 404 or a white screen, and scripts fail to load in the console.
How to check. Look at where the script links in index.html point. If they point to /assets/... while the site lives under /my-app/ — found it.
How to fix. Tell the bundler and the router where the root is. Per Vite's docs, for https://<USERNAME>.github.io/<REPO>/ you set base: '/<REPO>/' in vite.config.js. The router needs the same prefix — in React Router that's the basename option.
Short story-lessons, an agent simulator and daily practice — in our mobile app. Free.
Won't "everything to index.html" break real 404s?
It will, unless you do something: the server will start answering 200 for any nonexistent address. So your router needs its own "page not found" route for all unknown paths. How response codes differ is covered in what are HTTP status codes.
How do I tell whose 404 it is — the host's or my app's?
Look at the error page itself. If it's the host's standard placeholder without your design, the request never reached the app — fix the settings from causes 1–3. If the error is styled like your site, the rewrite already works and the router simply has no such route. Check the path spelling and your list of routes.
Why is it still 404 after the fix?
Check that the config file actually made it into the build: _redirects has to end up inside dist, not stay in the repository root. And make sure you deployed the new version and aren't looking at a cached old one.





