BusiRocket

Article

Lovable app shows 404 on refresh after deploying to Vercel or Netlify: the fix per host

Published on

You exported the app from Lovable, pushed it to GitHub and connected Vercel or Netlify. The home page works. You click through to /dashboard and that works too. Then you press refresh, or paste the link into a new tab, or a user opens the link from an email, and the host answers with a bare 404: NOT_FOUND.

The code is fine. The host is doing exactly what it was told, and nobody told it that this is a single-page app.

Why it happens

A Lovable app is a React app built with Vite. The build produces one HTML file, index.html, plus JavaScript and CSS in assets/. Every "page" of the app is drawn by JavaScript inside that one file: when you click a link, React Router changes the address bar without asking the server for anything.

A refresh is different. The browser asks the server for /dashboard. The static host looks for a file called dashboard or dashboard/index.html, finds none, and returns its 404 page. The JavaScript that knew what /dashboard meant never loads.

Lovable's own hosting handles this for you, which is why the published lovable.app address never showed it. Once the app runs somewhere else, the rule has to be written down for the new host.

First, rule out the other 404

If the home page itself returns 404, this article is not your problem yet. A 404 on / means the host is serving the wrong folder:

  • Vercel: in the project settings, the Framework Preset should be Vite and the Output Directory dist. If the repository holds the app in a subfolder, the Root Directory has to point at it.
  • Netlify: the build command is npm run build and the publish directory dist.

Fix that, redeploy, and come back if refreshing an inner page still fails.

Vercel

Create vercel.json in the root of the repository, next to package.json. This is the snippet from Vercel's own Vite documentation:

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

Commit, push, and let Vercel redeploy. Rewrites only apply when no real file matches the request, so /assets/index-abc123.js and /favicon.ico are still served as files. If the project sets cleanUrls: true, Vercel's docs say to write the destination as / instead of /index.html.

Netlify

Netlify reads a file called _redirects from the folder it publishes. In a Vite project, put it in public/, which Vite copies into dist on every build:

/*  /index.html  200

The 200 makes it a rewrite rather than a redirect: the address bar keeps /dashboard and the browser gets index.html. If you prefer configuration in netlify.toml, the same rule is:

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

If you already have other redirects, this one goes last. Like Vercel, Netlify does not shadow files that exist, so assets keep loading. Do not add the ! that forces the rule over existing files: with it, every JavaScript file would be answered with HTML and the app would go blank.

Cloudflare Pages and Workers

Cloudflare Pages treats a project without a top-level 404.html as a single-page app and serves index.html for unknown paths, so most Lovable exports work there without changes. If someone added a 404.html to public/, that switches the behaviour off; remove it and let the app draw its own not-found page.

On Workers with static assets, set it explicitly in wrangler.jsonc:

{
  "assets": {
    "directory": "./dist",
    "not_found_handling": "single-page-application"
  }
}

cPanel, Hostinger and other Apache hosting

Shared hosting usually runs Apache. Upload the contents of dist, not the folder itself, and add an .htaccess file next to index.html:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

The two conditions keep real files and folders untouched; everything else goes to index.html. Put the file in public/ so it survives the next build, and check that your upload tool does not skip dotfiles.

A VPS with nginx

In the server block that serves the app:

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

Reload nginx after the change with nginx -t && systemctl reload nginx.

GitHub Pages

GitHub Pages has no rewrite rules. The two workable options are switching the app to HashRouter, which puts routes after a # so the server only ever sees /, or copying index.html to 404.html at build time. The second keeps clean URLs but every deep link is answered with a 404 status, which search engines read as "this page does not exist". For anything public, a host with real rewrites is the better answer.

After the fix: give the app a real not-found page

Once every path returns index.html, the server never says 404 again, even for addresses that really do not exist. React Router needs a catch-all route, path="*", that renders a "page not found" screen, otherwise a mistyped link shows an empty layout. Google calls that a soft 404: a missing page that answers 200. For a dashboard behind a login it does not matter; for marketing pages you want indexed, it is worth knowing that a single-page app cannot send a true 404 status without server rendering.

Test it

Open a private window, paste a deep link such as /settings/billing directly into the address bar, and refresh it twice. Do the same with a link that carries a query string, such as the /auth/callback?code=... address Supabase sends people to after login. If that one loads but the user is still logged out, the next stop is Supabase login redirecting to localhost.

The 404 on refresh is one of six things that usually break between preview and production. It is the easy one: one file, one redeploy. If the app is past that and other things keep breaking, our vibe code rescue audit reads it end to end: 450 EUR, 48 working hours, a written report of what is broken and what each fix costs, with the fee deducted if you continue into the rescue (1,500-6,000 EUR, fixed scope).

Next step

Does this problem sound like yours?

Tell us about it and we will say how we would approach it, in what order and at what cost.