angular-ssr

Implement server-side rendering and hydration in Angular v20+ using @angular/ssr. Use for SSR setup, hydration strategies, prerendering static pages, and handling browser-only APIs. Triggers on SSR configuration, fixing hydration mismatches, prerendering routes, or making code SSR-compatible.

16 stars

Best use case

angular-ssr is best used when you need a repeatable AI agent workflow instead of a one-off prompt.

Implement server-side rendering and hydration in Angular v20+ using @angular/ssr. Use for SSR setup, hydration strategies, prerendering static pages, and handling browser-only APIs. Triggers on SSR configuration, fixing hydration mismatches, prerendering routes, or making code SSR-compatible.

Teams using angular-ssr should expect a more consistent output, faster repeated execution, less prompt rewriting.

When to use this skill

  • You want a reusable workflow that can be run more than once with consistent structure.

When not to use this skill

  • You only need a quick one-off answer and do not need a reusable workflow.
  • You cannot install or maintain the underlying files, dependencies, or repository context.

Installation

Claude Code / Cursor / Codex

$curl -o ~/.claude/skills/angular-ssr/SKILL.md --create-dirs "https://raw.githubusercontent.com/diegosouzapw/awesome-omni-skill/main/skills/frontend/angular-ssr/SKILL.md"

Manual Installation

  1. Download SKILL.md from GitHub
  2. Place it in .claude/skills/angular-ssr/SKILL.md inside your project
  3. Restart your AI agent — it will auto-discover the skill

How angular-ssr Compares

Feature / Agentangular-ssrStandard Approach
Platform SupportNot specifiedLimited / Varies
Context Awareness High Baseline
Installation ComplexityUnknownN/A

Frequently Asked Questions

What does this skill do?

Implement server-side rendering and hydration in Angular v20+ using @angular/ssr. Use for SSR setup, hydration strategies, prerendering static pages, and handling browser-only APIs. Triggers on SSR configuration, fixing hydration mismatches, prerendering routes, or making code SSR-compatible.

Where can I find the source code?

You can find the source code on GitHub using the link provided at the top of the page.

SKILL.md Source

# Angular SSR

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

## Setup

### Add SSR to Existing Project

```bash
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

```typescript
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

```typescript
// 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()`:

```typescript
// app.config.ts
import { provideClientHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(),
    // ...
  ],
};
```

### Incremental Hydration

Defer hydration of specific components:

```typescript
@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 PostComponent {
  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:

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

export const appConfig: ApplicationConfig = {
  providers: [
    provideClientHydration(withEventReplay()),
  ],
};
```

## Browser-Only Code

### Platform Detection

```typescript
import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Component({...})
export class MyComponent {
  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:

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

@Component({...})
export class ChartComponent {
  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

```typescript
// 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 StorageService {
  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

```typescript
// 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

```typescript
// 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:

```typescript
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

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

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

@Injectable({ providedIn: 'root' })
export class ProductService {
  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

```bash
# Build with SSR
ng build

# Output structure
dist/
├── my-app/
│   ├── browser/      # Client assets
│   └── server/       # Server bundle
```

### Run SSR Server

```bash
# Development
npm run serve:ssr:my-app

# Production
node dist/my-app/server/server.mjs
```

### Deploy to Node.js Host

```javascript
// 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](references/ssr-patterns.md).

Related Skills

frontend-angular-store

16
from diegosouzapw/awesome-omni-skill

Use when implementing state management with PlatformVmStore for complex components requiring reactive state, effects, and selectors.

frontend-angular-form

16
from diegosouzapw/awesome-omni-skill

Use when creating reactive forms with validation, async validators, dependent validation, and FormArrays using platform patterns.

frontend-angular-api-service

16
from diegosouzapw/awesome-omni-skill

Use when creating API services for backend communication with proper patterns for caching, error handling, and type safety.

angular

16
from diegosouzapw/awesome-omni-skill

Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns.

angular-v21-development

16
from diegosouzapw/awesome-omni-skill

Develop Angular v21 components, services, and directives with signals. Use when implementing standalone components, OnPush change detection, inject() function, and input()/output() functions.

angular-v17

16
from diegosouzapw/awesome-omni-skill

Angular 17. Proyecto usa este skill; contenido canónico en .ai-system.

angular-typescript-cursorrules-prompt-file-cursorrules

16
from diegosouzapw/awesome-omni-skill

Apply for angular-typescript-cursorrules-prompt-file. --- description: General rules for Angular components, focusing on code quality, performance, and maintainability. globs: **/*.component.ts

angular-tooling

16
from diegosouzapw/awesome-omni-skill

Use Angular CLI and development tools effectively in Angular v20+ projects. Use for project setup, code generation, building, testing, and configuration. Triggers on creating new projects, generating components/services/modules, configuring builds, running tests, or optimizing production builds.

angular-testing

16
from diegosouzapw/awesome-omni-skill

Write unit and integration tests for Angular v21+ applications using Vitest or Jasmine with TestBed, component harnesses, and modern testing patterns. Use for testing components with signals, OnPush change detection, services with inject(), and HTTP interactions. Triggers on test creation, testing signal-based components, mocking dependencies, or setting up test infrastructure.

angular-signals

16
from diegosouzapw/awesome-omni-skill

Implement signal-based reactive state management in Angular v20+. Use for creating reactive state with signal(), derived state with computed(), dependent state with linkedSignal(), and side effects with effect(). Triggers on state management questions, converting from BehaviorSubject/Observable patterns to signals, or implementing reactive data flows.

angular-rxjs-patterns

16
from diegosouzapw/awesome-omni-skill

Use when handling async operations in Angular applications with observables, operators, and subjects.

angular-router

16
from diegosouzapw/awesome-omni-skill

Angular Router for navigation, routing configuration, route guards, lazy loading, and parameter handling. Use when setting up routes, implementing navigation guards, lazy loading modules, handling route parameters, or implementing breadcrumbs and nested routes in Angular applications.