angular-ssr

Implement server-side rendering and hydration in Angular v20+ using @angular/ssr. Use for SSR setup, hydration strategies, prerendering static pages, and…

INSTALLATION
npx skills add https://github.com/analogjs/angular-skills --skill angular-ssr
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

Angular SSR

Implement server-side rendering, hydration, and prerendering in Angular v20+.

Setup

Add SSR to Existing Project

ng add @angular/ssr

This adds:

  • @angular/ssr package
  • server.ts - Express server
  • src/main.server.ts - Server bootstrap
  • src/app/app.config.server.ts - Server providers
  • Updates angular.json with SSR configuration

Project Structure

src/

├── app/

│   ├── app.config.ts          # Browser config

│   ├── app.config.server.ts   # Server config

│   └── app.routes.ts

├── main.ts                     # Browser bootstrap

├── main.server.ts              # Server bootstrap

server.ts                       # Express server

Configuration

app.config.server.ts

import { ApplicationConfig, mergeApplicationConfig } from '@angular/core';

import { provideServerRendering } from '@angular/platform-server';

import { provideServerRoutesConfig } from '@angular/ssr';

import { appConfig } from './app.config';

import { serverRoutes } from './app.routes.server';

const serverConfig: ApplicationConfig = {

  providers: [

    provideServerRendering(),

    provideServerRoutesConfig(serverRoutes),

  ],

};

export const config = mergeApplicationConfig(appConfig, serverConfig);

Server Routes Configuration

// app.routes.server.ts

import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [

  {

    path: '',

    renderMode: RenderMode.Prerender, // Static at build time

  },

  {

    path: 'products',

    renderMode: RenderMode.Prerender,

  },

  {

    path: 'products/:id',

    renderMode: RenderMode.Server, // Dynamic SSR

  },

  {

    path: 'dashboard',

    renderMode: RenderMode.Client, // Client-only (SPA)

  },

  {

    path: '**',

    renderMode: RenderMode.Server,

  },

];

Render Modes

Mode

Description

Use Case

RenderMode.Prerender

Static HTML at build time

Marketing pages, blogs

RenderMode.Server

Dynamic SSR per request

User-specific content

RenderMode.Client

Client-side only (SPA)

Authenticated dashboards

Hydration

Default Hydration

Hydration is enabled by default with provideClientHydration():

// app.config.ts

import { provideClientHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {

  providers: [

    provideClientHydration(),

    // ...

  ],

};

Incremental Hydration

Defer hydration of specific components:

@Component({

  template: `

    <!-- Hydrate when visible -->

    @defer (hydrate on viewport) {

      <app-comments [postId]="postId" />

    } @placeholder {

      <div class="comments-placeholder">Loading comments...</div>

    }

    <!-- Hydrate on interaction -->

    @defer (hydrate on interaction) {

      <app-interactive-chart [data]="chartData" />

    }

    <!-- Hydrate on idle -->

    @defer (hydrate on idle) {

      <app-recommendations />

    }

    <!-- Never hydrate (static only) -->

    @defer (hydrate never) {

      <app-static-footer />

    }

  `,

})

export class Post {

  postId = input.required<string>();

  chartData = input.required<ChartData>();

}

Hydration Triggers

Trigger

Description

hydrate on viewport

When element enters viewport

hydrate on interaction

On click, focus, or input

hydrate on idle

When browser is idle

hydrate on immediate

Immediately after load

hydrate on timer(ms)

After specified delay

hydrate when condition

When expression is true

hydrate never

Never hydrate (static)

Event Replay

Capture user events before hydration completes:

import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {

  providers: [

    provideClientHydration(withEventReplay()),

  ],

};

Browser-Only Code

Platform Detection

import { PLATFORM_ID, inject } from '@angular/core';

import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Component({...})

export class My {

  private platformId = inject(PLATFORM_ID);

  ngOnInit() {

    if (isPlatformBrowser(this.platformId)) {

      // Browser-only code

      window.addEventListener('scroll', this.onScroll);

    }

  }

}

afterNextRender / afterRender

Run code only in browser after rendering:

import { afterNextRender, afterRender } from '@angular/core';

@Component({...})

export class Chart {

  constructor() {

    // Runs once after first render (browser only)

    afterNextRender(() => {

      this.initChart();

    });

    // Runs after every render (browser only)

    afterRender(() => {

      this.updateChart();

    });

  }

  private initChart() {

    // Safe to use DOM APIs here

    const canvas = document.getElementById('chart');

    new Chart(canvas, this.config);

  }

}

Inject Browser APIs Safely

// tokens.ts

import { InjectionToken, PLATFORM_ID, inject } from '@angular/core';

import { isPlatformBrowser } from '@angular/common';

export const WINDOW = new InjectionToken<Window | null>('Window', {

  providedIn: 'root',

  factory: () => {

    const platformId = inject(PLATFORM_ID);

    return isPlatformBrowser(platformId) ? window : null;

  },

});

export const LOCAL_STORAGE = new InjectionToken<Storage | null>('LocalStorage', {

  providedIn: 'root',

  factory: () => {

    const platformId = inject(PLATFORM_ID);

    return isPlatformBrowser(platformId) ? localStorage : null;

  },

});

// Usage

@Injectable({ providedIn: 'root' })

export class Storage {

  private storage = inject(LOCAL_STORAGE);

  get(key: string): string | null {

    return this.storage?.getItem(key) ?? null;

  }

  set(key: string, value: string): void {

    this.storage?.setItem(key, value);

  }

}

Prerendering

Static Routes

// app.routes.server.ts

export const serverRoutes: ServerRoute[] = [

  { path: '', renderMode: RenderMode.Prerender },

  { path: 'about', renderMode: RenderMode.Prerender },

  { path: 'contact', renderMode: RenderMode.Prerender },

  { path: 'blog', renderMode: RenderMode.Prerender },

];

Dynamic Routes with getPrerenderParams

// app.routes.server.ts

import { RenderMode, ServerRoute, PrerenderFallback } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [

  {

    path: 'products/:id',

    renderMode: RenderMode.Prerender,

    async getPrerenderParams() {

      // Fetch product IDs to prerender

      const response = await fetch('https://api.example.com/products');

      const products = await response.json();

      return products.map((p: Product) => ({ id: p.id }));

    },

    fallback: PrerenderFallback.Server, // SSR for non-prerendered

  },

  {

    path: 'blog/:slug',

    renderMode: RenderMode.Prerender,

    async getPrerenderParams() {

      const posts = await fetchBlogPosts();

      return posts.map(post => ({ slug: post.slug }));

    },

    fallback: PrerenderFallback.Client, // SPA for non-prerendered

  },

];

Prerender Fallback Options

Fallback

Description

PrerenderFallback.Server

SSR for non-prerendered routes

PrerenderFallback.Client

Client-side rendering

PrerenderFallback.None

404 for non-prerendered routes

HTTP Caching

TransferState

Automatically transfer HTTP responses from server to client:

import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {

  providers: [

    provideClientHydration(

      withHttpTransferCacheOptions({

        includePostRequests: true,

        includeRequestsWithAuthHeaders: false,

        filter: (req) => !req.url.includes('/api/realtime'),

      })

    ),

  ],

};

Manual TransferState

import { TransferState, makeStateKey } from '@angular/core';

const PRODUCTS_KEY = makeStateKey<Product[]>('products');

@Injectable({ providedIn: 'root' })

export class Product {

  private http = inject(HttpClient);

  private transferState = inject(TransferState);

  private platformId = inject(PLATFORM_ID);

  getProducts(): Observable<Product[]> {

    // Check if data was transferred from server

    if (this.transferState.hasKey(PRODUCTS_KEY)) {

      const products = this.transferState.get(PRODUCTS_KEY, []);

      this.transferState.remove(PRODUCTS_KEY);

      return of(products);

    }

    return this.http.get<Product[]>('/api/products').pipe(

      tap(products => {

        // Store for transfer on server

        if (isPlatformServer(this.platformId)) {

          this.transferState.set(PRODUCTS_KEY, products);

        }

      })

    );

  }

}

Build and Deploy

Build Commands

# Build with SSR

ng build

# Output structure

dist/

├── my-app/

│   ├── browser/      # Client assets

│   └── server/       # Server bundle

Run SSR Server

# Development

npm run serve:ssr:my-app

# Production

node dist/my-app/server/server.mjs

Deploy to Node.js Host

// server.ts (generated)

import { APP_BASE_HREF } from '@angular/common';

import { CommonEngine } from '@angular/ssr/node';

import express from 'express';

import { dirname, join, resolve } from 'node:path';

import { fileURLToPath } from 'node:url';

import bootstrap from './src/main.server';

const serverDistFolder = dirname(fileURLToPath(import.meta.url));

const browserDistFolder = resolve(serverDistFolder, '../browser');

const indexHtml = join(serverDistFolder, 'index.server.html');

const app = express();

const commonEngine = new CommonEngine();

app.get('*', express.static(browserDistFolder, { maxAge: '1y', index: false }));

app.get('*', (req, res, next) => {

  commonEngine

    .render({

      bootstrap,

      documentFilePath: indexHtml,

      url: req.originalUrl,

      publicPath: browserDistFolder,

      providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }],

    })

    .then((html) => res.send(html))

    .catch((err) => next(err));

});

app.listen(4000, () => {

  console.log('Server listening on http://localhost:4000');

});

For advanced patterns, see references/ssr-patterns.md.

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card