404 on page refresh after deploying a Bolt or Lovable app to Vercel or Netlify
Updated 2026-11-11
What you see
You export a React + Vite app from Bolt or Lovable and deploy it. https://yourapp.com/ works, and clicking around works. But refreshing on /dashboard, or opening a link someone shared, gives a 404.
Why it happens
A single-page app (SPA) ships one index.html, and the JavaScript router (often React Router) draws /dashboard in the browser. When you navigate by clicking, the browser never asks the server for /dashboard. On a refresh it does, and the host looks for a file at /dashboard, finds nothing and returns 404.
Vercel’s docs say this directly: when a Vite app is deployed as an SPA, “deep linking won’t work out of the box.”
Fix it on Vercel
Create vercel.json in the project root (next to package.json):
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"rewrites": [
{ "source": "/(.*)", "destination": "/index.html" }
]
}
If you set "cleanUrls": true in the same file, use / as the destination instead of /index.html.
Fix it on Netlify
Create public/_redirects (Vite copies public/ into the build output) containing:
/* /index.html 200
or add this to netlify.toml:
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
Status 200 makes it a rewrite: the URL stays the same and Netlify serves index.html. Netlify won’t shadow files that actually exist, so your JS, CSS and images still load. If you have other redirect rules, put this one last.
Gotchas
- Commit the file. AI builders sometimes generate it in the editor but leave it out of the export.
- The file must end up at the root of the build output (
dist/). For Netlify that meanspublic/_redirectsin a Vite project, notsrc/_redirects. - A rewrite will also turn real missing pages into your app’s shell. Add a catch-all
*route in your router that renders a proper “not found” screen.
Check it worked
Deploy, open https://yourapp.com/some/deep/route in a new tab, and refresh. The app should render that route.