A normal Angular app sends an almost empty HTML file and builds the page in the browser. Search engines and AI crawlers may not run your JavaScript well, and users see a blank screen until the code loads. Server-side rendering (SSR) produces the full HTML on the server first. The browser then hydrates it: Angular attaches to the existing HTML instead of rebuilding it.
What you will learn
- The three render modes: client, server and prerender
- How to add SSR and set titles and meta tags
- How to write code that is safe on the server
Add SSR
In an existing app:
ng add @angular/ssr
The schematic adds these pieces (names from a project we generated and built for this lesson):
src/main.server.tsandsrc/app/app.config.server.ts: the server entry and configuration.src/app/app.routes.server.ts: which render mode each route uses.src/server.ts: a small Express server that renders pages and serves static files.provideClientHydration()inapp.config.ts, which turns on hydration in the browser.outputMode: "server"inangular.json.
New projects can start with SSR: ng new my-app --ssr.
Choose a render mode per route
// File: src/app/app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
// Built once at build time: best for pages that rarely change
{ path: '', renderMode: RenderMode.Prerender },
// Rendered on each request: best for pages that depend on live data
{ path: 'articles/:slug', renderMode: RenderMode.Server },
// Rendered in the browser only: best for private dashboards
{ path: 'dashboard', renderMode: RenderMode.Client },
{ path: '**', renderMode: RenderMode.Server },
];
- Prerender (SSG): HTML files are created during
ng build. Fastest and cheapest. Needs known URLs, or a list of them for dynamic paths. - Server (SSR): HTML is created on each request. Always fresh, and costs server time.
- Client (CSR): the classic mode. Good where SEO does not matter, such as behind a login.
Most sites mix all three.
Titles and meta tags
Search engines read the HTML the server returns, so set titles and descriptions in the component or on the route:
// File: src/app/pages/article.ts
import { Component, OnInit, inject, input } from '@angular/core';
import { Meta, Title } from '@angular/platform-browser';
@Component({
selector: 'app-article',
template: `
<h1>{{ slug() }}</h1>
<p>This text is in the server-rendered HTML.</p>
`,
})
export class Article implements OnInit {
private readonly title = inject(Title);
private readonly meta = inject(Meta);
readonly slug = input.required<string>();
ngOnInit(): void {
this.title.setTitle(`Article: ${this.slug()}`);
this.meta.updateTag({ name: 'description', content: `Reading ${this.slug()}` });
}
}
In our test build, curl on /articles/hello returned a <title>, the description tag and the <h1> in the raw HTML, which is what crawlers see. Also add a canonical link and Open Graph tags for pages that are shared.
Code that is safe on the server
The server has no window, document or localStorage. Browser-only code must not run during server rendering:
// File: src/app/pages/browser-only.ts
import { Component, afterNextRender, signal } from '@angular/core';
@Component({
selector: 'app-browser-only',
template: `<p>Window width: {{ width() }}</p>`,
})
export class BrowserOnly {
protected readonly width = signal(0);
constructor() {
// Skipped on the server, runs in the browser after rendering.
afterNextRender(() => this.width.set(window.innerWidth));
}
}
In the test build the server HTML showed the starting value ("not yet" for a similar component), and the browser filled in the real value after hydration.
Other tips:
- Use
afterNextRenderorisPlatformBrowserfor browser APIs. - Avoid random values or current times in templates. Different server and browser output causes hydration mismatches.
- Do not read cookies or user data into shared state on the server, or one visitor's data may be sent to another. Server state must be per request.
- Use
HttpClientwithwithFetch()and enable HTTP transfer cache (included withprovideClientHydration()) so data loaded on the server is not fetched again in the browser.
Hydration and its limits
Hydration reuses the server HTML. If your markup changes the DOM directly (for example with innerHTML or a jQuery plugin) the browser structure can differ from the server HTML, and Angular logs a mismatch. For components that cannot hydrate, add ngSkipHydration to the element, as a last resort.
Also consider incremental hydration with @defer (hydrate on viewport), which delays hydrating heavy parts of a page. It is covered in the Angular documentation, and the syntax is evolving, so check the current docs.
Build, run and deploy
ng build
node dist/<project-name>/server/server.mjs
The server listens on port 4000, or on PORT. Run it with a process manager such as pm2 behind Nginx.
### Allowed hosts (Angular's SSRF protection)
Recent Angular versions refuse requests whose Host header is not on an allow-list. When we ran the production server locally and called it with curl localhost, it answered 400 Bad Request: Header "host" is not allowed. Fix it in angular.json:
"security": {
"allowedHosts": ["example.com", "www.example.com"]
}
For tests you can also set the NG_ALLOWED_HOSTS environment variable, for example NG_ALLOWED_HOSTS=localhost node dist/app/server/server.mjs. Behind a proxy, make sure Nginx passes the correct Host header (proxy_set_header Host $host;).
When to use SSR
- Yes: public content that should rank in search or be read by AI crawlers: blogs, docs, product pages, landing pages.
- Maybe: apps where first paint speed matters a lot on slow phones.
- No: private dashboards behind a login. Plain client rendering is simpler and cheaper.
Try it yourself
- Run
ng add @angular/ssron a copy of one of your projects. - Prerender the home page and server-render a dynamic page, then use
curlto see the HTML. - Find a place that uses
localStorageand move it intoafterNextRender.
Quick check
- What does hydration do?
- Which render mode fits a page behind a login?
- How do you run code only in the browser?
Next, continue with Angular SEO: Title, Meta Tags, Canonical URLs, Lazy Images and SSR.