React on StaticHost is a static export problem
Create React App, Vite + React, and other bundlers compile JSX into HTML/JS/CSS files. Those files can live on StaticHost. What cannot live here is a fantasy nginx rule that maps every unknown path to index.html. The server is strict: try_files $uri $uri/ =404. History-mode React Router deep links 404 unless matching files exist.
This guide covers production build, upload, and the routing constraint without pretending StaticHost is Netlify.
Build the artifact
Vite + React (recommended modern path):
npm ci
npm run build
Output: dist/ with index.html and assets/. Set base: '/' in vite.config. Public env: VITE_*. Details: Vite build hosting.
Create React App:
npm ci
npm run build
Output: build/ (not dist). Homepage/public URL: set "homepage": "." or "homepage": "/" in package.json as appropriate so asset paths resolve. Env prefix: REACT_APP_. More: upload a build folder.
Zip the contents of dist or build:
cd dist # or cd build
zip -r ../react-site.zip .
Deploy the zip or a GitHub branch that already contains these files. StaticHost does not run npm. Nested folder trap: zip without 404.
React Router: choose a mode that matches the server
HashRouter (works with =404 servers):
import { HashRouter, Routes, Route } from 'react-router-dom'
export function App() {
return (
<HashRouter>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
</Routes>
</HashRouter>
)
}
URLs look like /#/about. The server only sees /, finds index.html, and the client reads the hash.
BrowserRouter (history mode) requires either:
- a host that rewrites to
index.html(StaticHost does not), or - pre-rendering/SSG that emits real files per route (e.g. a static export pipeline).
Refreshing /about on StaticHost with only a CSR BrowserRouter app → 404. That is correct behavior for this product, not a misconfiguration you can ticket away. Broader context: host a single-page application.
Verification on HTTPS preview
- Deploy; open preview (TLS immediate).
- Confirm JS bundle loads.
- Navigate in-app to a nested route, then hard refresh.
- If you use
BrowserRouterand step 3 404s, switch toHashRouteror add SSG—do not invent a rewrite. - Roll back from deploy history if a release breaks.
Custom domain: add DNS, wait for verification, wait for certificate. Rebuild when REACT_APP_* / VITE_* must change for production API URLs. Pro ($30) staging helps keep staging keys out of production bundles.
What not to host
- Next.js apps that need
next start—use static export only whenoutput: 'export'applies. - Server Functions / RSC-only deployments.
- Anything expecting PHP.
Plans: Starter $9 (1 site, 2 GB), Pro $30 (3 sites, 10 GB, staging), Scale $65 (10 sites, 30 GB), Business $130 (30 sites, 100 GB, teams). Trial ~1 day. No forever-free, email, cPanel, or built-in CDN.
Client-only data
Fetch APIs from the browser with CORS configured for your preview and final hostnames. Auth tokens in localStorage are an app decision with known risks; StaticHost neither helps nor stores them server-side.
Worked example: Vite React marketing site with a docs subpath
You want / and /docs/getting-started as shareable URLs.
Option A (hash): wrap the app in HashRouter. Accept /#/docs/getting-started. Build, zip dist, deploy, verify hard refresh on the hash URL.
Option B (files): use a prerender plugin or split docs into a small Astro/Eleventy subtree emitted as real HTML beside the React island. Confirm docs/getting-started/index.html exists on disk before upload.
Commands:
npm ci
export VITE_API_URL=https://api.example.com
npm run build
cd dist && zip -r ../react-marketing.zip .
Upload to StaticHost. Open HTTPS preview—not vite preview alone—as the source of truth. Paste the docs URL with a hard navigation. Only then add the custom domain (DNS → verify → cert). If a release breaks the bundle, roll back; fix locally; rebuild—StaticHost still will not run npm for you.
Failure table: React on StaticHost
| Symptom | Likely cause | Fix |
|---|---|---|
Refresh /about → 404 | BrowserRouter without files | HashRouter or prerender |
| Blank white screen | Wrong base/homepage; JS 404 | Fix config; rebuild; flatten zip |
| API works on localhost only | CORS / wrong VITE_* baked in | Rebuild with prod API; allowlist origins |
_redirects ignored | Not Netlify | Stop relying on it |
| Staging secrets in prod bundle | One build reused everywhere | Separate builds; Pro staging |
| Next app fails | Needs Node server | Static export only or other host |
| Source maps public | sourcemap: true in prod | Disable for public deploys |
Extra procedure: router decision in the PR template
Require every frontend PR that touches routing to answer:
- Are we on
HashRouterorBrowserRouter? - If
BrowserRouter, which paths are emitted as files indist/build? - Did CI curl those paths on HTTPS preview and assert 200?
- Did CI curl a known-missing path and assert 404 (proving we are not imagining fallback)?
Shipping React to StaticHost without that checklist is how launch-week support fills with “it works when I click around.” Plans, trial length, and non-features (no email, no CDN product, no forever-free) do not change the router physics. For broader static JS context see static hosting for JavaScript apps.
Code splitting and empty route shells
React.lazy and route-based code splitting are fine on StaticHost because the extra chunks are still static files under assets/. What is not fine is assuming a lazy route’s URL exists on the server. The HTML shell and the chunk URLs must 200; the path /settings still 404s on hard refresh unless you prerendered a file or you use hashes.
After each production build, open the HTTPS preview Network panel and confirm lazy chunks load when you navigate in-app. Then paste the deep URL cold. That two-step check catches “works when I click” false confidence. If a chunk 404s, your base/homepage is wrong or the zip omitted assets/.
Keep error boundaries for failed chunk loads so users see a retry message instead of a blank screen when a deploy races a long-lived tab. Roll back via deploy history if a release references chunks that never made it into the artifact. None of this requires remote npm, SPA fallback, email, cPanel, or a CDN product—only a complete dist/build tree and an honest router strategy.
FAQ
Does homepage in CRA fix deep link 404s?
No. It fixes asset path prefixes. Deep link 404s are the missing SPA fallback.
Can I add a public/_redirects file?
StaticHost’s nginx will not adopt Netlify redirect syntax as a general rule engine. Assume no SPA fallback.
Is React still “static hosting”?
Yes when you ship static files. React runs in the browser after download.
Should I commit the build folder?
Optional for deploy branches. Prefer reproducible npm run build in CI.
How do I host a React marketing site without a router?
Multiple HTML entry points or a single landing page with sections—no router required. See static landing page hosting.