Angular Server-Side Rendering (SSR) and Hydration: Fast, SEO-Friendly Pages

Angular Tutorial: Beginner to Advanced · 17. Advanced routing, rendering and SEO · 5 min read · Updated

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.ts and src/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() in app.config.ts, which turns on hydration in the browser.
  • outputMode: "server" in angular.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 afterNextRender or isPlatformBrowser for 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 HttpClient with withFetch() and enable HTTP transfer cache (included with provideClientHydration()) 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

  1. Run ng add @angular/ssr on a copy of one of your projects.
  2. Prerender the home page and server-render a dynamic page, then use curl to see the HTML.
  3. Find a place that uses localStorage and move it into afterNextRender.

Quick check

  1. What does hydration do?
  2. Which render mode fits a page behind a login?
  3. How do you run code only in the browser?

Next, continue with Angular SEO: Title, Meta Tags, Canonical URLs, Lazy Images and SSR.

Frequently asked questions

Does SSR fix SEO problems on its own?

No. It makes content available in the HTML. You still need good titles, descriptions, headings, links, sitemaps and fast pages.

Can I use SSR with a Node server I already have?

Yes. The generated server is Express-based, and the Angular app engine can be mounted in other Node servers. Check the guide for the current adapter APIs.