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

Free static html hosting explainedWhat static hosting does and does not do, the limits that surprise people, and how to tell wFree HTML file hosting, comparedWhat free HTML file hosting actually provides, the four routes worth considering, and the chHow to turn a zip into a websiteAn archive of pages becomes a site when the folder structure and the paths agree. What has tChoosing a Netlify alternativeDeployment platforms are built around a pipeline. If your page has no build, most of what yoRelative vs absolute paths in HTML: why references break when a page movesRelative vs absolute paths in HTML: 'next to this file' versus 'at this location on the web'Cache busting: when your update does not show upCache busting is changing a file's address when the file changes, so browsers holding the olAngular in a single HTML fileRun Angular from one HTML file with no install: live examples, the four parts every page neeTypeScript in HTML: test HTML, CSS, and typed code in one fileSee how HTML, CSS, and TypeScript work in one file, test the live result and its empty-inputSvelteKit 3 and single HTML filesSvelteKit 3 is still a prerelease, and SvelteKit needs a build either way. What does run in Next.js and HTML: from a Next.js page to a plain HTML fileHow Next.js turns your pages into HTML, how to export static HTML files, and how to rebuild Material UI in a single HTML fileRun Material UI in one HTML file with no build step: an import map, React from jsDelivr and Chakra UI in a single HTML fileRun Chakra UI 3 in a single HTML file: an import map, React from jsDelivr and a ChakraProvidVue in a single HTML fileRun Vue 3 from a single HTML file with one script tag and no build step. A live example to ePreact in one HTML file: a score keeper you can shareBuild a Preact score counter and leaderboard in a single HTML file with no build step: imporWebpack from npm: from install to one HTML pageInstall webpack from npm, bundle your JavaScript, and end up with an HTML page that runs. LiA JSX example in React, in one HTML fileA JSX example you can run from one HTML file: React and Babel from a CDN, a live counter, thJSFiddle React: the same app as one HTML fileHow React runs in JSFiddle and how to get the same app into one HTML file: three script tagsSolidJS vs React, and how to run Solid in one HTML fileHow SolidJS differs from React, and how to run Solid in a single HTML file with no build steUse Babel in a single HTML fileBabel is a JavaScript compiler. Load it from a CDN and a text/babel script tag turns JSX and