Publish a Single-Page App
A single-page app with client-side routing—React Router, Vue Router, SvelteKit's static adapter, anything with real URLs—has one hosting requirement: when a visitor lands directly on /dashboard/settings, the server must hand them the app shell instead of a 404, and let the router take it from there.
On Surge, that behavior is a file name.
The 200.html file
Name your app shell 200.html (or ship a copy of index.html under that name) and Surge serves it for every path that doesn't match a real file—with a 200 status, at the URL the visitor asked for. The router boots, reads the address bar, and renders the right view. Deep links work. Refresh works. Shared links work.
{
"scripts": {
"build": "vite build && cp dist/index.html dist/200.html",
"deploy": "npm run build && surge ./dist publish"
}
}
That cp is the entire integration. Works identically for any bundler—the only requirement is that the output directory ends up containing 200.html.
Why 200 and not 404 tricks
Some hosts fake SPA routing by serving the shell from the 404 page. That answers deep links with a 404 status—search engines and monitoring see errors on every route. Surge's 200.html answers with 200, because your routes aren't errors. Keep 404.html for what it's for: paths your app genuinely doesn't know.
Publish it
npm run deploy
The whole app—shell, assets, both special files—propagates to every edge node during the publish. Every route of your app is served from the node nearest each visitor, because every route is the shell, and the shell is everywhere.
Your first publish writes the domain to a CNAME file in the output directory; move it to wherever your bundler copies static files verbatim (public/ for Vite and Astro) so it survives rebuilds—details in the Vite guide.
Hash routing needs nothing
If your router uses # URLs, every path is really /, and plain index.html already handles it. 200.html is for routers using the History API—which is what you want anyway, and now it costs one file copy.