You deployed your React app. The homepage loads fine. Navigation works perfectly. Then you share a link like /about or /privacy-policy with someone - and they get a 404.
This is one of the most common deployment mistakes with Single Page Applications. Here's exactly what's happening and how to fix it.
What's a SPA and Why Does This Happen?
A React app (or any SPA built with Vue, Angular, etc.) ships one single HTML file - index.html. Every "page" you see - home, about, contact - is not a real file on the server. They are virtual routes, rendered in the browser by JavaScript.
This creates two completely different types of navigation:
Clicking a link inside the app
JavaScript intercepts the click, swaps out the component, and updates the URL - all without touching the server. Everything works fine.
Typing a URL directly or hitting refresh
The browser sends a real HTTP request to the server asking for that path. The server looks for a file called /privacy-policy. It doesn't exist. So it returns 404 - before React even loads.
That's the core of the problem. The server doesn't know about your client-side routes.
Diagnosing It
A quick way to confirm this is the issue: if even /index.html returns 404, the server can't find your build files at all - which means your output directory is misconfigured on top of the routing problem.
Vite builds into a dist/ folder. If your hosting platform is looking somewhere else, nothing will be served.
The Fix
Tell the server: "For any URL that doesn't match a real file, serve index.html and let JavaScript handle it."
Here's how to do it on the most common platforms:
Digital Ocean App Platform
Add catchall_document to your app spec, and make sure output_dir matches your build tool's output folder (dist for Vite, build for Create React App):
Netlify
Create a _redirects file inside your public/ folder (so it gets copied into dist on build):
Vercel
Add a vercel.json to your project root:
Nginx
In your server block config:
Apache
In your .htaccess file:
Why It Works in Development but Not Production
Vite's dev server (npm run dev) handles this automatically - it serves index.html for every unknown route out of the box. So the problem never shows up locally, and catches many developers off guard on first deployment.
Quick Checklist
- Build output directory is correctly set (dist for Vite, build for CRA)
- A catchall/fallback is configured to serve index.html for unknown routes
- The _redirects or config file is inside public/ so it's included in the build output
This is a one-time configuration fix. Once it's in place, all your routes will work correctly - whether someone clicks a link, types a URL directly, or hits refresh.