tuqo

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.html at the root means a multi-page site: unknown paths get that page with a 404 status, and there is no fallback to index.html;
  • no 404.html means an SPA: unknown paths get index.html with 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.

What’s next