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 buildand the publish directorydist.
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).