SPA 404 on page refresh (React Router, Vue Router)
A familiar situation: a React or Vue SPA works, navigation between pages is smooth, but as
soon as you reload an inner page (say, /about), the browser shows a 404. Here’s
why it happens and how to fix it.
Why the 404 appears
In a single-page application (SPA), routing is done by JavaScript in the browser, not by
the server. The host really has only one index.html. When you follow a link inside the
app, React Router or Vue Router swaps the address in the address bar without contacting
the server.
But when you reload /about, the browser asks the server for a file at that path.
There is no such file, only index.html, so the server answers 404.
The fix: fall back to index.html
The server needs to return index.html for any unknown path, and the router then shows
the right page. This is called an SPA fallback.
On a regular host you set it up by hand (in nginx, try_files $uri /index.html; on
Apache, through .htaccess).
On Tuqo it works out of the box
If your build has an index.html and client-side routing, Tuqo automatically serves
index.html for inner routes. No configuration needed: deploy the SPA and reloading
/about just works.
npm run build # produces dist/ or build/ with index.html
tar -czf site.tar.gz -C dist .
# deploy it: the index.html fallback turns on by itself
You can also upload the project with its package.json: Tuqo runs the build on Node 20
and publishes dist, build or out.
When you need a real 404
If you have a multi-page static site (not an SPA) where every page is a separate file, you
don’t want the index.html fallback: a missing address should honestly return 404. Tuqo
tells these cases apart by the structure of the build:
- a
404.htmlat the root means a multi-page site: unknown paths get that page with a 404 status, and there is no fallback toindex.html; - no
404.htmlmeans an SPA: unknown paths getindex.htmlwith a 200 status.
So if your SPA template happens to ship a 404.html at the root, remove it, or reloading
inner routes will show that page instead of your app.
Missing files with an extension (a lost .css or .js) always get a plain 404, never
index.html, so a broken asset link stays visible instead of silently loading HTML.