Skip to content

First 5 fixes are $29 in exchange for a short testimonial.

Claim a $29 fix
Ship It Live

Guides

Vite or React SPA returns 404 on refresh

The home page loads on Vercel or Netlify, but refreshing a client route returns 404. Serve index.html for those paths with a rewrite, without hiding real asset files.

Updated 2026-10-15

You open the deployed site, click Dashboard, and the screen is right. You refresh, or you paste /dashboard into a new tab, and the host shows its own 404. The Lovable or Bolt preview never did this, because the preview is a dev server that already knows the route. Vercel and Netlify are serving files. A Vite React app, unless you opted into a server framework, builds one index.html and lets the browser router paint the rest.

The request the host actually sees

The first visit requests /. The publish folder contains index.html, so the response is 200 and React starts. React Router or TanStack Router then calls history.pushState. The address bar changes to /dashboard and no document is requested. A refresh is a new document request for /dashboard. There is no dashboard.html and no dashboard/index.html. The platform returns 404. The JavaScript that would have rendered the page never runs.

That is also why deep links from email fail, and why the Supabase redirect can land on a 404 even after the Site URL is correct. The auth redirect arrives at /auth/callback as a full page load.

Vercel: the rewrite from the Vite docs

Vercel's Vite documentation says deep linking will not work out of the box for an SPA, and it tells you to add a vercel.json at the project root:

{
  "$schema": "https://openapi.vercel.sh/vercel.json",
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

If cleanUrls is true in that same file, Vercel says to drop the extension and use /index as the destination instead of /index.html. Commit vercel.json next to package.json, not inside src or dist. Dist is wiped on every build. Redeploy after the file is on the branch the project builds.

Do not add this rewrite to a Next.js project. Next already routes /dashboard. Do not add it to TanStack Start or another Vite server build either. Those apps have a server entry, and a blanket rewrite to the client index.html will swallow it. Lovable's typical React SPA, and Bolt's typical Vite SPA, are the cases this file is for. If the framework preset on Vercel is wrong, fix that in the output directory guide before debugging the rewrite. A rewrite cannot serve index.html that was never published.

Netlify: a 200 rewrite, last in the file

Netlify turns a redirect into a rewrite when the status is 200. The address bar stays on /dashboard while the server returns index.html. For a single-page app using pushState, Netlify's docs give this rule, and they say to list it last if you have other rules:

/*  /index.html  200

The same rule in netlify.toml at the repo root:

[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

The _redirects file has to end up in the folder you publish. Vite copies public/ into dist/ on build, and dist is the publish directory for a default Vite app. Put the file at public/_redirects with no file extension. A _redirects left only at the repo root is not inside dist, so Netlify never sees it. Rules in _redirects run before rules in netlify.toml. The first match wins, so keep this catch-all at the bottom.

Netlify does not shadow a real file unless you force the rule. Their docs show that /* /index.html 200 still serves /assets/index-abc123.js if that file exists. Do not add the exclamation mark (200!). Forcing the rewrite sends JavaScript and CSS requests at index.html as well, and the app stays blank for a new reason.

Show that refresh is a 200

  1. Deploy the commit that contains vercel.json or public/_redirects.
  2. Open https://your-app.example/, navigate to a client route with a click, then refresh. You should see the app, not the platform 404 page.
  3. In a terminal, curl -sI https://your-app.example/dashboard should report 200, and the body should be your index.html (a div id="root" or the app title), not the host's 404 document.
  4. Confirm an asset still loads: the script URL from the page source should return JavaScript, with a content type of JavaScript, not HTML. If the script is HTML, the rewrite is forced over real files.
  5. Try the path you use after login, not only /dashboard. A callback at /auth/callback is the one Supabase will refresh.

A 200 on refresh with a white page is a different bug. The route is being served, and the client threw while starting. That is the missing VITE_ env case. A 404 only on refresh, with a working click-through, is this rewrite.

Questions that come up next

Why does the home page work if the other routes 404?

The host has a real file at /index.html, so / succeeds. A Vite client router then changes the address bar without asking the server. A refresh asks the server for that path. There is no file at that path, so Vercel or Netlify returns 404 unless a rewrite serves index.html.

Will the catch-all rewrite break JavaScript and CSS files?

Not when you use the documented form. Netlify will not shadow a file that exists unless you force the rule with an exclamation mark. Leave the exclamation mark off, and put the SPA rule last. Vercel's Vite documentation uses a rewrite to /index.html for paths the SPA handles. Hashed files under /assets still exist on disk and should keep their URLs.

Should I add this to a Next.js app or TanStack Start app?

No. This is for a client-rendered Vite SPA, including React Router and TanStack Router in SPA mode. Next.js and TanStack Start already have a server. A rewrite of every path to index.html fights that server and hides real 404s.