How to host a built front end

The build output is already static files. What breaks after uploading is routing and the base path, in that order.

A production build is a folder of static files. Hosting it is copying that folder somewhere, and then fixing two things that only appear once it is served.

A build output folder showing an entry file and hashed asset files.
A build output folder showing an entry file and hashed asset files.

This guide covers routing, the base path, and caching.

Routing: the refresh 404

The application handles navigation in the browser. Only the entry file exists as an actual file.

So visiting the site and navigating works. Refreshing on a sub-path, or opening a link to one directly, asks the server for a path that does not exist, and it returns a 404.

The fix is a rewrite rule: any path that is not a real file serves the entry file, and the application takes over from there.

Every static host supports this. The configuration file differs; the rule is the same.

/*    /index.html   200

This is the single most common problem with a deployed front end, and it is invisible during development because the development server does it automatically.

The base path

Builds assume they will be served from the root of a domain, so asset paths start with a slash.

Deployed to a subfolder, every one of those resolves to the wrong place. The entry file loads, finds nothing, and the page is blank with console errors.

Set the base path in the build configuration before building. Not afterwards, because the paths are baked into the output.

Symptom Cause
Blank page, console errors Base path wrong
Refresh gives 404 No rewrite rule
Old version after deploy Entry file cached
Works in dev, not deployed Both of the above

Secrets

Anything in a front-end build ships to the browser. Environment variables included in a bundle are readable by anyone who opens the page.

Build tools often warn about this and the warning is routinely ignored, because the variable is needed and it appears to work.

If a value must stay private, it belongs behind a server. There is no configuration that makes a bundled secret secret.

Public identifiers, analytics keys and public API endpoints are fine.

A rewrite rule sending unknown paths to the entry file.
A rewrite rule sending unknown paths to the entry file.

Caching after deploy

Asset filenames usually include a content hash, so a changed file gets a new name and caching handles itself.

The entry file does not. If it is cached, returning visitors load an old entry file pointing at asset names that may no longer exist, and they see either the old version or a broken one.

Serve the entry file with a no-cache header. Assets can be cached aggressively because their names change.

A copy per person ✗ Each edit lives on one machine ✗ No way to merge the changes ✗ Nobody can say which is current ✗ The oldest copy keeps circulating One address ✓ Everyone opens the same page ✓ A correction is seen by all ✓ There is only one current version ✓ Forwarding shares the page, not a copy
The same document as a file and at an address, a revision later.

Test the build, not the dev server

The development server rewrites routes, serves from the root, and reloads on change. None of those are true of the deployed version.

Build it, serve the output folder locally, and open it. Most deployment problems appear there, before anyone else sees them.

Closely related: Free static html hosting explained, and Free HTML file hosting, compared for the adjacent problem.

Put it at an address

Set the base path before building, add the rewrite for client-side routes, keep secrets out of the bundle, serve the entry file with no-cache, and test the built output rather than the dev server.

Then the deployed build behaves the way it did on your machine.

Questions people ask

Does a built front end need a server?

Not to serve it. The build produces static files. A server is needed only if the application talks to something private, which is a separate concern from hosting.

Why does refreshing a sub-page give a 404?

Because client-side routing means only the entry file really exists. The server is asked for a path that is not a file. The fix is a rule sending unknown paths to the entry file.

Why are all my assets missing under a subfolder?

The build assumed it would be served from the root, so asset paths start with a slash. Set the base path in the build configuration to the subfolder.

Where do environment values go?

Anything in a front-end build is public once shipped. Values that must stay private belong on a server, not in the bundle.

Why do users see an old version after deploying?

Caching. Hashed filenames handle assets automatically; the entry file should be served with a no-cache header so it always fetches the current asset names.

Keep reading