matevalidate.com Scan your site free →

← All guides

Clicking works, refreshing 404s

Every link inside the app works. Then somebody refreshes on /dashboard, or opens a link you sent them, and gets 404 — page not found. The page exists — they were just looking at it.

Why this happens to every single-page app

A single-page app is one HTML file. React Router (or any client router) changes the URL and swaps what is on screen — inside the already-loaded page. The server is not involved, which is why clicking works.

Refreshing — or opening a deep link, or a search engine visiting — asks the server for /dashboard. The server looks for a file called that, finds none (there is only index.html), and answers 404. Both halves are behaving correctly; they have never been introduced.

The fix is one rule on the server: for any URL that matches no real file, serve index.html — the app then boots and its router shows the right page.

The rule, host by host

Netlify — a file named _redirects in the folder that gets published (for Vite: public/, so it lands in dist/):

/*  /index.html  200

The 200 matters: it makes this a rewrite, not a redirect — the URL stays /dashboard.

Vercelvercel.json at the project root:

{ "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] }

(For frameworks Vercel detects, it often configures this itself — add the file when it has not.)

Azure Static Web Appsstaticwebapp.config.json:

{ "navigationFallback": { "rewrite": "/index.html", "exclude": ["/assets/*"] } }

The exclude list is worth reading twice: anything not excluded falls back to the app's HTML — including, if you are not careful, files that should be served as themselves, like robots.txt. (This site runs on Static Web Apps; that exclude list is not hypothetical.)

GitHub Pages — there is no rewrite support. The honest options: use a HashRouter (URLs become /#/dashboard, no server lookup involved), or the 404.html trick — a copy of index.html served on any miss. If the site lives under /repo-name/, that is a second problem — base paths.

nginx — in the site's location /:

try_files $uri $uri/ /index.html;

Two edges that bite later

Hand it to the AI

Paste this to the AI that built your app

My deployed single-page app 404s on refresh and on deep links. Host:
[Netlify / Vercel / Azure Static Web Apps / GitHub Pages / nginx]. The build
output folder is [dist/build]. Add the SPA fallback configuration for this
host so unknown routes serve index.html with status 200, while real files
and /api/* are excluded from the fallback. Then confirm the router renders a
proper not-found page for genuinely wrong URLs.

Prove it

Deploy, then: refresh on a deep route (stays on the page, no 404), open the same route in a private window from a pasted link (same), and request a URL that truly does not exist (the app's own not-found page, not a blank screen). Three checks, ten seconds each.

Questions people also ask

Why does clicking to the page work when refreshing it does not?

Clicking never leaves the loaded app — the router changes the URL and renders the page itself, with no server involved. Refreshing asks the server for that URL as a file, and the server has no such file: a single-page app has exactly one HTML file.

Is this a React Router bug?

No. The router is doing its half correctly; the server was never told about the arrangement. The fix is one rewrite rule on the server, not a code change.

Will the rewrite break my API routes or real files?

Configured properly, no: real files are matched first (or explicitly excluded, on hosts that support it), and only URLs with no matching file fall back to index.html. Keep API paths out of the fallback so a missing endpoint still 404s honestly.