Page works, but refreshing it gives a 404

You'll see:
  • Home page loads, but /dashboard or /login shows “Page not found” when refreshed or opened directly
  • Netlify “Page not found”, Vercel 404: NOT_FOUND, or an Apache 404 on shared hosting

Why it happens

Single-page apps (React + Vite, React Router) have one real file — index.html. Routes like /dashboard exist only in JavaScript. When you refresh, the host looks for a file called /dashboard, doesn't find one and returns 404. You need a rewrite that serves index.html for unknown paths.

How to fix it

  1. Add the fallback rule for your host

    Pick your host below. Put the file in your project's public/ folder (Vite copies it into dist/) and redeploy.

  2. Don't redirect real files

    Use a rewrite (status 200), not a 301/302 redirect, and make sure it's the last rule so real assets like /assets/app.js still load.

Fallback rule for each host

Vercel

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

Vercel docs (opens in a new tab)

Netlify

public/_redirects (ends up in dist/_redirects)
/*  /index.html  200

Netlify docs (opens in a new tab)

Cloudflare Pages

(none)
# No config needed: if there is no top-level 404.html,
# Cloudflare Pages serves index.html for unknown routes.
# If you DO ship a 404.html, add public/_redirects with:
/*  /index.html  200

Cloudflare Pages docs (opens in a new tab)

Railway

Caddyfile (or serve -s dist)
:{$PORT:3000} {
    root * dist
    encode gzip
    file_server
    try_files {path} /index.html
}

Railway docs (opens in a new tab)

Render

Dashboard → Redirects/Rewrites (or render.yaml)
# Dashboard rule: Source /*  Destination /index.html  Action Rewrite
routes:
  - type: rewrite
    source: /*
    destination: /index.html

Render docs (opens in a new tab)

Fly.io

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

Fly.io docs (opens in a new tab)

DigitalOcean App Platform

App → Settings → your static site → Custom Pages
# Custom Pages → Catchall → Page Name: index.html

DigitalOcean App Platform docs (opens in a new tab)

Hostinger

public_html/.htaccess (Node.js web app deploys generate this automatically)
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]

Hostinger docs (opens in a new tab)

Replit Static deployment (.replit)

[[deployment.rewrites]]
from = "/*"
to = "/index.html"

Docs (opens in a new tab)

Common with:

Related fixes