npx skills add https://github.com/analogjs/angular-skills --skill angular-ssrHow Angular Ssr fits into a Paperclip company.
Angular Ssr drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.
Pre-configured AI company — 18 agents, 18 skills, one-time purchase.
SKILL.md435 linesExpandCollapse
---name: angular-ssrdescription: 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.--- # Angular SSR Implement server-side rendering, hydration, and prerendering in Angular v20+. ## Setup ### Add SSR to Existing Project ```bashng 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 bootstrapserver.ts # Express server``` ## Configuration ### app.config.server.ts ```typescriptimport { 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.tsimport { 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.tsimport { 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 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: ```typescriptimport { provideClientHydration, withEventReplay } from '@angular/platform-browser'; export const appConfig: ApplicationConfig = { providers: [ provideClientHydration(withEventReplay()), ],};``` ## Browser-Only Code ### Platform Detection ```typescriptimport { 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: ```typescriptimport { 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 ```typescript// tokens.tsimport { 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 ```typescript// app.routes.server.tsexport 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.tsimport { 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: ```typescriptimport { 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 ```typescriptimport { 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 ```bash# Build with SSRng build # Output structuredist/├── my-app/│ ├── browser/ # Client assets│ └── server/ # Server bundle``` ### Run SSR Server ```bash# Developmentnpm run serve:ssr:my-app # Productionnode 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).Angular Component
The angular-component skill generates modern Angular v20+ standalone components using signal-based inputs and outputs, OnPush change detection, and host binding
Angular Di
Covers Angular v20+ dependency injection patterns with modern inject() syntax instead of constructor injection. Shows provider configuration at root, component,
Angular Directives
Solid reference for building Angular directives the modern way with v20+ patterns. Covers attribute directives using the new `input()` function and `host` prope