1. Manual Deployment
npm run build
# or equivalently:
ng build
The production build outputs to dist/<project-name>/browser/. This folder contains only static files — upload it to any static host (Netlify, Vercel, S3, GitHub Pages, Nginx, etc.) following that host's instructions.
Server configuration for SPA routing
Angular is a Single Page Application — only index.html exists on disk. If a user refreshes /tasks/42 or pastes a deep link, the server must return index.html for any path that doesn't match a static file, otherwise it returns a 404.
Nginx:
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
}
Apache (.htaccess):
RewriteEngine On
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -f [OR]
RewriteCond %{DOCUMENT_ROOT}%{REQUEST_URI} -d
RewriteRule ^ - [L]
RewriteRule ^ /index.html
2. Deployment with CLI
The Angular CLI has first-class integrations with common hosting providers. Add the provider's package once, then deploy with a single command:
ng add @angular/fire # Firebase Hosting
ng add @netlify-builder/deploy # Netlify
ng add angular-cli-ghpages # GitHub Pages
ng deploy
The builder handles the production build and upload automatically.
3. Server-Side Rendering (SSR)
Setup
Add SSR to an existing project:
ng add @angular/ssr
Or start a new project with it included:
ng new my-app --ssr
ng add @angular/ssr creates these additional files:
| File | Purpose |
|---|---|
server.ts |
Node.js / Express server entry point |
main.server.ts |
Server-side bootstrap |
app.config.server.ts |
Server-specific providers (e.g. provideServerRendering) |
app.routes.server.ts |
Per-route rendering mode configuration (Angular 19+) |
After building, the dist/ folder has two subfolders:
dist/my-app/
browser/ ← static assets and client JS
server/ ← Node.js server bundle
Run the server:
node dist/my-app/server/server.mjs
Rendering modes (Angular 19+)
Since Angular 19, you configure how each route is rendered in app.routes.server.ts. There are three modes:
RenderMode.Prerender — SSG (Static Site Generation)
Angular runs at build time and generates a ready-made HTML file for the route, stored in dist/browser/. The server just sends that file — no Angular processing at runtime. Fastest possible response, works on any static host, great SEO. Only suitable for content that is the same for all users.
RenderMode.Server — SSR (Server-Side Rendering)
Angular runs on the Node.js server for every incoming request and generates HTML on the fly. Supports user-specific or real-time content. Requires a running Node.js server. Slower than SSG but more flexible.
RenderMode.Client — CSR (Client-Side Rendering)
The server sends a bare index.html. Angular bootstraps entirely in the browser. No SEO benefit. Use for auth-gated pages or routes that depend on browser APIs.
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{ path: '', renderMode: RenderMode.Prerender }, // / → pre-built index.html
{ path: 'about', renderMode: RenderMode.Prerender }, // /about → pre-built about/index.html
{ path: 'profile', renderMode: RenderMode.Server }, // /profile → rendered per request
{ path: 'tasks', renderMode: RenderMode.Client }, // /tasks → rendered in browser
];
Default behavior: Routes without dynamic segments (:param) default to Prerender. Routes with dynamic segments default to Server, because Angular cannot know all possible param values at build time.
Prerendering dynamic routes is possible if you provide the param values explicitly:
{
path: 'tasks/:id',
renderMode: RenderMode.Prerender,
getPrerenderParams: async () => [{ id: '1' }, { id: '2' }, { id: '3' }],
}
This generates dist/browser/tasks/1/index.html, dist/browser/tasks/2/index.html, etc. at build time.
Before Angular 19
Older versions had no per-route control. The whole app was either SSR or fully static, configured globally via outputMode in angular.json.
4. Pitfalls
Browser-only code (localStorage, DOM)
Code that accesses browser APIs (localStorage, window, DOM manipulation) will crash on the server during SSR because those APIs don't exist in Node.js.
Use afterNextRender() to run code exclusively in the browser — it is skipped entirely on the server:
import { Component, afterNextRender } from '@angular/core';
@Component({ ... })
export class ThemeComponent {
constructor() {
afterNextRender(() => {
const saved = localStorage.getItem('theme');
// safe — this only runs in the browser
});
}
}
Resolvers in SSR
Resolvers run on the server during the first render — the data is fetched server-side and serialized into the HTML. This means:
- The resolver must not use browser-only APIs
- HTTP calls in resolvers during SSR are made from the Node.js server, not the browser
- On subsequent client-side navigations (after hydration), the resolver runs normally in the browser
If your resolver uses localStorage or window, guard it with isPlatformBrowser():
import { inject, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import { ResolveFn } from '@angular/router';
export const myResolver: ResolveFn<string> = () => {
if (isPlatformBrowser(inject(PLATFORM_ID))) {
return localStorage.getItem('key') ?? '';
}
return '';
};
Top comments (0)