Deploy

Fix 404 on Page Refresh in a Vite + React App (Vercel, Netlify, Cloudflare)

8 min read · Updated October 16, 2026 · Checked against official docs

You deploy your app. The home page loads. You click around and everything works. Then you refresh on /dashboard, or send someone a link to /pricing, and get:

404: NOT_FOUND

or “Page not found” from your host. Nothing is wrong with your code. Your host just doesn’t know your app is a single-page app, and needs one small rule to send every page to index.html.

This guide explains why it happens, then gives the exact fix for each common host. First, though, one check that saves a lot of confusion.

First: is your app actually a single-page app?

The fix below is for apps that build to plain static files: a dist/ folder with one index.html and some JavaScript. That covers:

  • Vite + React apps using React Router (BrowserRouter), which is what many Bolt projects and older Lovable projects are.
  • Create React App projects, and Vue or Svelte apps with client-side routing.

It does not apply to apps that run server code:

  • Newer Lovable projects. Lovable’s docs say projects created from May 13, 2026 use TanStack Start, which needs a host that runs its server part. To tell which you have, open package.json: a TanStack Start project lists @tanstack/react-start. (Lovable: Deploying and hosting outside Lovable)
  • Next.js, Remix, Nuxt, SvelteKit and similar frameworks, which handle routes on the server.

If you’re in the second group and getting 404s, the cause is usually the build or deploy settings, not a missing rewrite. Adding a catch-all rewrite there can hide the real problem.

Why refresh breaks a single-page app

A single-page app has exactly one real HTML file. When you click a link inside the app, React Router swaps the page in the browser. The server is never asked for /dashboard.

When you refresh or open a link directly, the browser asks the server for /dashboard. The server looks for a file at that path, finds nothing, and returns 404 before your app’s JavaScript ever loads.

The fix is a rewrite: “if a request doesn’t match a real file, serve index.html instead (with a 200 status).” Your app then loads and React Router shows the right page. Real files like your JavaScript, CSS and images are still served normally, because hosts check for an actual file first.

Vercel

Create a file called vercel.json in the root of your project, next to package.json:

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

This is the configuration from Vercel’s own Vite guide, which notes that deep linking won’t work out of the box for SPA Vite apps. (Vercel: Vite on Vercel)

If your project also has Vercel Functions under /api, keep them out of the rewrite:

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

Commit, push, and let Vercel redeploy. For the rest of the Vercel setup (environment variables, domains), see How to Deploy a Cursor or Bolt Project to Vercel.

Netlify

Create a file called _redirects (no extension) in your project’s public/ folder. Vite copies everything in public/ into dist/ at build time, which is where Netlify needs it:

/*    /index.html   200

The 200 is important. It makes this a rewrite (the address bar keeps /dashboard), not a redirect to the home page. Netlify’s docs show this exact rule for single-page apps. (Netlify: Rewrites and proxies)

Prefer config files? The same rule in netlify.toml:

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

Cloudflare Pages

Cloudflare Pages has a helpful default: if your build output has no top-level 404.html, Pages assumes you’re deploying a single-page app and serves your root page for any path that doesn’t match a file. (Cloudflare: Serving Pages)

So if you’re getting 404s on Pages, check:

  1. Is there a 404.html in your dist/ folder? Some templates add one. If it’s there, Pages switches to normal “not found” behavior. Remove it if you want SPA routing.
  2. Is the build output directory set to dist? If Pages is serving the wrong folder, nothing will route correctly.

Cloudflare Workers (static assets)

If you deploy with Wrangler to Workers instead of Pages, tell it your app is a single-page app in your Wrangler config:

// wrangler.jsonc
{
  "name": "my-app",
  "compatibility_date": "2026-10-01",
  "assets": {
    "directory": "./dist",
    "not_found_handling": "single-page-application"
  }
}

Or in wrangler.toml:

[assets]
directory = "./dist"
not_found_handling = "single-page-application"

Cloudflare’s docs explain that in this mode, navigation requests that don’t match a file get your /index.html. (Cloudflare: Single Page Application)

Nginx (your own server or Docker)

Use try_files so Nginx tries the real file, then a folder, then falls back to index.html:

location / {
    try_files $uri $uri/ /index.html;
}

This is the same rule Lovable’s docs give for self-hosting older React + Vite apps.

Other hosts

The idea is always the same: “serve index.html with a 200 for paths that aren’t files.” Look in your host’s docs for “SPA”, “rewrite” or “fallback”. Avoid using a storage bucket’s “error page” setting for this. Lovable’s docs warn against it because those pages are served with a 404 status code, so every deep link still reports “not found” even though the app appears.

Ask your AI tool

My Vite + React app returns 404 when I refresh any page except the home page
after deploying to [Vercel / Netlify / Cloudflare Pages / Cloudflare Workers].
First confirm this is a client-side single-page app (no server rendering, no
@tanstack/react-start). If so, add only the SPA fallback config for that host
so unknown paths serve /index.html with status 200. Don't change routing code.

After the fix: a few things to double-check

Assets fail to load on nested pages. If /dashboard now loads a blank page and the console shows JavaScript files 404ing from /dashboard/assets/..., your build is using relative asset paths. In vite.config.ts, base should be '/' (the default) for sites served from the root of a domain.

Real 404s become “soft 404s”. With a catch-all rewrite, a typo like /dashbord also returns your app with a 200 status. Add a catch-all route in React Router that shows a “Page not found” screen:

<Route path="*" element={<NotFound />} />

Search engines may still treat these as real pages. Google recommends either redirecting to a URL that returns a true 404 or adding a noindex robots meta tag on the not-found view. (Google: JavaScript SEO basics)

Sign-in breaks after moving hosts. If login worked on the old URL but email links now go to localhost or the old domain, that’s Supabase’s redirect settings. See Supabase Confirmation Email Redirects to localhost? Here’s the Fix.

Blank page everywhere, not just on refresh. That’s usually missing environment variables (for example, VITE_SUPABASE_URL wasn’t set on the new host, and VITE_ variables are baked in at build time). See Environment Variables and API Keys, Explained.

Test it like a visitor

  1. Open your live site in a private window.
  2. Type a deep link straight into the address bar, like https://yourdomain.com/dashboard. It should load.
  3. Refresh on three or four different pages.
  4. Visit a made-up path like /this-does-not-exist. You should see your app’s “not found” screen, not a host error.
  5. Paste a deep link into a chat app and check the preview shows your site.

Official sources used: Vercel: Vite on Vercel · Netlify: Rewrites and proxies · Cloudflare: Serving Pages · Cloudflare: Single Page Application · Lovable: Deploying and hosting outside Lovable · Google: JavaScript SEO basics. Checked October 2026.

Want the whole launch path in one place?

The Ship It Kit bundles copy-paste prompts, step-by-step deploy guides and a pre-launch checklist for Lovable, Bolt, Cursor and Replit apps. One-time $19. Launch price: $19 $17 until November 14, 2026.

Get the Ship It Kit, $19$17 See what's inside

Spotted something out of date? Platforms change their screens often. The official docs linked above are always the source of truth.