Deploying to Static Hosts
A client-rendered Angular application is, after the production build, just a folder of static files: an index.html, hashed JavaScript bundles, CSS, and assets. That makes static hosts like Netlify, Vercel, Firebase Hosting, and GitHub Pages an excellent, low-cost, low-maintenance home for single-page apps. The one detail you must get right on every host is SPA fallback routing — telling the host to serve index.html for any unmatched path so the Angular router can take over on the client.
How a static Angular build works
Running the production build emits a self-contained bundle into the output directory. With the application builder (the default since Angular 17), client assets land in dist/<project>/browser.
ng build
Output:
Initial chunk files | Names | Raw size | Estimated transfer size
main-7QZ4FK2A.js | main | 210.44 kB | 58.12 kB
styles-5INURTSO.css | styles | 18.30 kB | 3.10 kB
Output location: dist/my-app/browser
Application bundle generation complete. [4.812 seconds]
The browser folder is what you upload. Everything in it is fingerprinted, so it can be cached aggressively — except index.html, which must always be served fresh so clients pick up new bundle hashes.
If you are setting a non-root deployment path (for example a GitHub Pages project site), build with
--base-hrefso generated asset URLs resolve correctly:ng build --base-href "/my-repo/".
Why SPA fallback routing matters
The Angular router uses the HTML5 History API, producing clean URLs like /products/42. When a user navigates inside the app that works fine, but if they reload that URL or open it directly, the host receives a request for /products/42 — a path that has no corresponding file. Without a fallback rule, the host returns a 404.
The fix is universal: rewrite every unmatched request to /index.html with a 200 status. Angular boots, reads the URL, and renders the right route. Each host expresses this rule differently.
Netlify
Add a _redirects file to the build output, or a netlify.toml at the repo root.
[build]
command = "ng build"
publish = "dist/my-app/browser"
[[redirects]]
from = "/*"
to = "/index.html"
status = 200
The status = 200 (not 301/302) is what makes it a rewrite rather than a redirect, preserving the original URL in the address bar.
Vercel
Vercel auto-detects Angular, but you can pin the configuration with vercel.json.
{
"buildCommand": "ng build",
"outputDirectory": "dist/my-app/browser",
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]
}
Firebase Hosting
Use the Angular CLI deploy integration, which wires up Firebase end to end.
ng add @angular/fire
ng deploy
That generates a firebase.json. The critical block is the rewrite:
{
"hosting": {
"public": "dist/my-app/browser",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"rewrites": [{ "source": "**", "destination": "/index.html" }]
}
}
GitHub Pages
GitHub Pages has no rewrite engine, so the common trick is a 404.html that mirrors index.html. The angular-cli-ghpages package automates the build, the base href, and the 404 copy.
ng add angular-cli-ghpages
ng deploy --base-href=/my-repo/
Output:
📦 Building "my-app"
🔧 Building application bundle...
🌐 Deploying to gh-pages branch...
🚀 Successfully published via angular-cli-ghpages!
Host comparison
| Host | Fallback mechanism | Custom domain | Build integration |
|---|---|---|---|
| Netlify | _redirects / netlify.toml 200 | Yes (free TLS) | Git auto-deploy |
| Vercel | rewrites in vercel.json | Yes (free TLS) | Git auto-deploy |
| Firebase | rewrites in firebase.json | Yes (free TLS) | ng deploy |
| GitHub Pages | 404.html copy of index.html | Yes (free TLS) | angular-cli-ghpages |
Caching headers
Because bundle filenames are content-hashed, serve them with a long, immutable cache while keeping index.html uncached. On Netlify, add a _headers file:
/index.html
Cache-Control: no-cache
/*.js
Cache-Control: public, max-age=31536000, immutable
/*.css
Cache-Control: public, max-age=31536000, immutable
A stale
index.htmlis the most common “users see the old app after deploy” bug. Always sendno-cache(revalidate) forindex.htmlso the browser re-fetches the latest bundle hashes.
Best Practices
- Publish the
browsersubfolder of the dist output, not the parentdistdirectory. - Configure the SPA fallback rewrite with a
200status so the URL is preserved and the router can resolve it. - Set
--base-hrefwhenever the app is served from a subpath, and verify asset URLs in the deployedindex.html. - Cache hashed bundles for a year as
immutable, but serveindex.htmlwithno-cache. - Prefer Git-connected auto-deploys (Netlify/Vercel) or
ng deploy(Firebase) over manual uploads to keep deployments reproducible. - Remember that static hosting serves client-rendered HTML only; if you need server rendering for SEO or first-paint, deploy an SSR target instead.