The constraint in one line

StaticHost serves files with nginx logic equivalent to try_files $uri $uri/ =404. There is no SPA fallback that maps unknown paths to /index.html. If you deploy a browser-router app and someone refreshes /dashboard/settings, the server looks for that file, fails, and returns 404.

This is intentional product behavior, not a missing checkbox. This guide explains how to host SPAs anyway—with hash routing or with generated files per route—and how to stop copying _redirects advice from other vendors.

What an SPA upload contains

Typically:

index.html
assets/index-….js
assets/index-….css

One HTML shell; JavaScript paints the UI and owns routing. On hosts with fallback, every deep link serves the shell. On StaticHost, only / (and real files) work unless you change approach.

Strategy A — Hash routing (smallest change)

React:

import { HashRouter } from 'react-router-dom'
// wrap routes in HashRouter

Vue:

import { createWebHashHistory, createRouter } from 'vue-router'
createRouter({ history: createWebHashHistory(), routes })

URLs look like https://example.com/#/dashboard/settings. The browser requests /; index.html loads; the router reads the hash. Works with =404 servers. Downsides: hash URLs are slightly uglier; some analytics setups need hash listening.

Strategy B — Generate a file per route (SSG / prerender)

Use a static export or prerender step so /dashboard/settings/index.html (or equivalent) exists. Next output: 'export', SvelteKit adapter-static with prerender, Astro static routes, or prerender plugins for Vite SPAs.

Then history URLs are real files. try_files succeeds. This is the clean long-term approach for public marketing URLs.

Do not “Strategy C — invent a rewrite.” There isn’t one here.

Deploy steps (any strategy)

npm ci
npm run build
cd dist   # or build/out
zip -r ../spa.zip .

Upload to StaticHost or push artifact branch—no remote npm. Verify on HTTPS preview:

  1. Load /
  2. Navigate in-app to a deep route
  3. Copy that URL, open a new tab, paste, enter
  4. Hard refresh

For hash mode, step 3–4 should work. For history mode without files, step 3–4 404—expected. Fix strategy before ads or store listings. Rollback via deploy history if you ship the wrong mode.

Related: React, Vue, Vite, Svelte static, Next export.

APIs, auth, and CORS

SPAs call APIs elsewhere. Allowlist preview and production origins. Tokens in the browser are visible to users—use the appropriate OAuth/public key patterns. StaticHost does not run your API.

Plans

Starter $9 (1×2 GB), Pro $30 (3×10 GB + staging), Scale $65 (10×30 GB), Business $130 (30×100 GB + teams). Trial ~1 day. No forever-free, cPanel, email, built-in CDN, PHP.

When to pick another host

If hash URLs are unacceptable, SSG is impossible, and you refuse to emit files, you need a host that documents SPA fallback. Choose it knowingly—where to host, wrong choice.

Worked example: Vite + React admin that must survive refresh

You ship an internal admin at admin.example.com. Product wants clean paths like /users/42. SEO does not matter; refresh must not 404.

Path 1 — fastest ship: switch the router to HashRouter. Build, zip dist, deploy to StaticHost, open HTTPS preview, paste https://preview…/#/users/42, hard refresh—200. Attach the custom domain only after that passes. Cert issues after DNS verifies.

Path 2 — clean URLs: add a prerender step (or migrate the public subset to a static-capable metaframework) so users/42/index.html exists for routes you care about. Private, auth-gated routes that cannot be prerendered still cannot exist as files—those screens must live behind hash routes or on an app host.

What fails: uploading a CRA/Vite BrowserRouter build unchanged, then filing a ticket for “SPA mode.” StaticHost will keep returning 404 for missing paths. That is the product.

Failure table: SPA deploys on StaticHost

SymptomLikely causeFix
/ works; refresh on /settings 404sHistory mode without filesHashRouter or prerender /settings
Assets 404 after deployWrong base / homepageSet root base: '/' or CRA homepage; rebuild
Blank page on previewJS error or nested zipConsole check; flatten zip root
API calls fail on custom domain onlyCORS allowlist missed new originAllow https://your-domain
_redirects uploaded, still 404Netlify syntax ≠ this nginxRemove fantasy; change routing strategy
Service worker pins old shellAggressive caching on HTTPSBump SW version; unregister while debugging
Staging shows prod API dataSame env baked into both buildsRebuild with distinct VITE_* / REACT_APP_*

Print the table near your CI docs. Deep-link QA belongs in the pipeline, not in production panic.

Extra procedure: CI smoke test for routing honesty

Add a job after deploy (or against HTTPS preview) that fails the pipeline when architecture drifts:

# hash-mode expectation: deep path without hash should 404 OR you only test hash URLs
curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL/#/settings" || true

# history-mode WITH prerender: real file must 200
code=$(curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL/settings")
test "$code" = "200"

# history-mode WITHOUT prerender on StaticHost: document 404 as expected
code=$(curl -s -o /dev/null -w "%{http_code}" "$PREVIEW_URL/settings")
test "$code" = "404"

Pick one assertion block that matches your chosen strategy. The point is not cruelty—it is refusing to discover SPA mismatch when a customer bookmarks a screen. Combine with deploy history rollback so a bad router merge can be undone in minutes while you fix main.

Remember: StaticHost does not run npm ci. Your Actions (or other CI) builds the artifact; the host only publishes files. No forever-free tier after the short trial; plan quotas still apply to SPA shells the same as brochure sites. No email, no cPanel, no built-in CDN product—wire APIs and asset weight accordingly.

Authenticated SPAs and bookmarkability

Logged-in apps love deep links: support tickets, settings tabs, project IDs. Those links must survive refresh on StaticHost via hash routing or prerendered shells that then hydrate and check auth client-side. Prerendering a private page’s HTML skeleton is OK if it contains no secrets; the API still authorizes. Never bake access tokens into static files.

Document for QA: “open bookmark in a fresh browser profile while logged out, then while logged in.” Logged out may show a login screen shell; logged in should land on the route after auth redirect logic you control in JS. If your auth library assumes server sessions on the same origin, you are edging toward wrong choice territory. Keep APIs elsewhere, files here, and nginx’s =404 honesty in the architecture review.

FAQ

Does 404.html equal SPA fallback?

No. A custom 404 document is not a silent rewrite of all unknown paths to your app shell.

Will a _redirects file fix this?

Do not assume Netlify-style redirects apply. Treat StaticHost as =404.

Is hash routing bad for SEO?

For app UIs behind login, SEO rarely matters. For public content, prefer SSG files with history URLs.

Can I use both hash and clean URLs?

Pick one primary strategy to avoid duplicate content chaos.

Does HTTPS preview behave differently from custom domains for routing?

No. Same file rules. Domains only change the hostname and cert.