Org Skills

angular-ssr

Design and build server-side rendering, prerendering and hydration for Angular — per-route `RenderMode` in `app.routes.server.ts`, `provideServerRendering(withRoutes(...))`, `getPrerenderParams`, `provideClientHydration()` with incremental hydration and `@defer (hydrate on …)`, the HTTP transfer cache, browser-only code with `afterNextRender`, `outputMode`, `AngularNodeAppEngine` and CDN caching.

Download .zip Raw Source
When agents use itUse this whenever the user runs `ng add @angular/ssr`, edits `app.routes.server.ts`, `app.config.server.ts` or `server.ts`, asks whether a route should be prerendered or server-rendered, hits NG0500/NG0502 hydration mismatches, `window is not defined` or `document is not defined` on the server, sees the client refetch everything the server already loaded, or asks about SEO, app shell or streaming. Also use it when a slow first paint on a public page turns out to be a rendering-mode decision.

Install

Copilot (VS Code, Visual Studio, Copilot CLI and github.com) reads skills from the repository — commit them so the whole team gets them.

npx skills add AGCO-Global/org-skills --skill angular-ssr -a github-copilot
# or with the org installer (adds .github/skills/angular-ssr):
npx -y github:AGCO-Global/org-skills add skill angular-ssr

Uses the open skills CLI. Works with Claude Code, Codex, Cursor, Copilot, Gemini CLI, OpenCode, Windsurf and 60+ others — it asks which agent to install into.

npx skills add AGCO-Global/org-skills --skill angular-ssr
# user-level instead of project-level:
npx skills add AGCO-Global/org-skills --skill angular-ssr -g

Installs the frontend-angular-skills plugin, which bundles all Frontend / Angular skills and keeps them updated.

/plugin marketplace add AGCO-Global/org-skills
/plugin install frontend-angular-skills@org-skills
# or just this skill, in this repository:
npx skills add AGCO-Global/org-skills --skill angular-ssr -a claude-code

Use Download .zip above, then upload it under Customize → Skills → Upload skill. Team and Enterprise admins can sync this repo as a plugin marketplace instead.

Installs into .agents/skills/, which Codex reads.

npx skills add AGCO-Global/org-skills --skill angular-ssr -a codex

Installs into Cursor's skills folder.

npx skills add AGCO-Global/org-skills --skill angular-ssr -a cursor

Installs into Gemini CLI's skills folder.

npx skills add AGCO-Global/org-skills --skill angular-ssr -a gemini-cli

Any tool with rules, instructions or custom prompts: use Copy SKILL.md above and paste it in. It is plain Markdown.

Commands use your normal git sign-in to GitHub, so they work while the repository is private. Node.js 20+ required.

Try it — example prompts

Prompts this skill is tested against, and what a good answer includes.

  • We added @angular/ssr to our Angular 22 site and the browser still fetches every API call again after hydration. Why?

    the HTTP transfer cache is on by default with provideClientHydration() requests with Authorization, Proxy-Authorization or Cookie headers are excluded by default a different client URL (base URL, cache-busting param) misses the cache POST requests need includePostRequests warns that includeRequestsWithAuthHeaders leaks data if the HTML is CDN-cacheable

  • Which routes of our marketing site plus logged-in dashboard should be prerendered, server-rendered, or client-rendered? Angular 22.

    RenderMode.Prerender for content identical for everyone RenderMode.Server for request-dependent content, with a Cache-Control header RenderMode.Client for routes behind authentication app.routes.server.ts with a ServerRoute array getPrerenderParams for parameterised prerendered routes with a fallback mentions outputMode static if no route needs a server

  • I get NG0500 hydration mismatch on our product page. How do I fix it?

    lists likely causes: invalid HTML the browser repairs, direct DOM manipulation, Date.now/Math.random in the template, locale or timezone formatting reproduce in a clean profile with extensions disabled fix the cause rather than suppressing ngSkipHydration only for third-party widgets that own their DOM

  • Our SSR page ships JavaScript for the footer and the article body even though nothing there is clickable.

    @defer (hydrate never) around the static regions explains the block still renders on the server @placeholder with reserved height where a hydrate trigger is used lists the other hydrate triggers (on viewport, on interaction, on idle, when)

  • Our Angular component crashes the server render with 'document is not defined'. It measures an element's width.

    afterNextRender({ read: () => ... }) for the measurement never runs on the server, which is why it is the right hook inject(DOCUMENT) instead of the global isPlatformBrowser(inject(PLATFORM_ID)) only when the branch is needed before render

Skill contents

Angular SSR and Hydration

Targets: Angular 22 (LTS 20 and 21 noted where different) · Verified: 2026-09 against https://angular.dev/guide/ssr, https://angular.dev/guide/hydration, https://angular.dev/guide/incremental-hydration

Server rendering is a per-route decision, not an application-wide switch: most apps want a few prerendered marketing routes, a few server-rendered dynamic ones, and everything behind the login client-rendered. Hydration then has one job — reuse the server's DOM and its data instead of throwing both away. Every SSR bug worth debugging comes from code that assumed a browser, or from data the client fetched twice.

1. Decide first

Question Default Change when
Does this app need SSR at all? No, for an authenticated internal app — CSR is simpler and there is nothing to index Public content, SEO, link previews, or LCP on a slow network matters
Per route RenderMode.Prerender for content that is the same for everyone Personalised or request-dependent → RenderMode.Server. Behind auth or highly interactive → RenderMode.Client
outputMode "server" (default) — a Node server handles requests Every route is prerenderable → "static", deploy to a CDN with no server at all
Hydration provideClientHydration() alone on v22 — incremental hydration and event replay are included Genuine mismatch you cannot fix → withNoIncrementalHydration() as a temporary measure with a ticket
Data fetched during SSR Leave the transfer cache on; the client reuses the server's GET/HEAD responses Per-user data behind an Authorization header — it is excluded by default; opt in only with includeRequestsWithAuthHeaders and a deliberate decision
Browser APIs afterNextRender({ read: … }) Needed before render → isPlatformBrowser(inject(PLATFORM_ID)), with a server-side fallback value
Below-the-fold islands @defer (hydrate on viewport) Never interactive → @defer (hydrate never), which ships no JavaScript for it at all

2. Setting it up

ng add @angular/ssr

This adds server.ts, app.config.server.ts and app.routes.server.ts, and sets outputMode in angular.json.

// app.routes.server.ts
import { RenderMode, type ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },
  { path: 'products/:slug', renderMode: RenderMode.Prerender, async getPrerenderParams() {
      const products: { slug: string }[] = await fetch('https://api.example.com/products').then((r) => r.json());
      return products.map(({ slug }) => ({ slug }));   // one object per page, every param keyed
    } },
  { path: 'search', renderMode: RenderMode.Server, headers: { 'Cache-Control': 'public, max-age=60' } },
  { path: 'app/**', renderMode: RenderMode.Client },
];

app.config.server.ts wires it up with provideServerRendering(withRoutes(serverRoutes)) (plus withAppShell(AppShell) if a client-rendered area wants a prerendered shell). A ServerRoute also takes status and fallback (PrerenderFallback.Server by default, or Client / None) — the fallback decides what happens to a :slug that getPrerenderParams did not return, which is the difference between a working long tail and a wall of 404s. Route-mode choice, prerendering at scale, cache headers and deployment shape: references/rendering-modes.md.

3. Hydration

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

providers: [provideClientHydration()];

On v22 that one call enables DOM reconciliation, the HTTP transfer cache and incremental hydration, which in turn enables event replay. So on v22: delete withIncrementalHydration() (deprecated, removal intended in v24) and delete withEventReplay() (redundant). On v20/21 both are still needed explicitly.

Then make islands hydrate on demand:

@defer (hydrate on viewport) { <app-reviews [productId]="id()" /> }
@placeholder { <div class="reviews-skeleton" style="min-height: 320px"></div> }

@defer (hydrate never) { <app-footer /> }

Triggers: hydrate on idle, on viewport, on interaction, on hover, on immediate, on timer(…), hydrate when <expr>, hydrate never. The block renders fully on the server either way; the trigger only decides when its JavaScript loads. hydrate never on genuinely static regions — footer, legal text, a rendered article body — is the cheapest win available, and every @defer needs a @placeholder with reserved height or you have traded JavaScript for layout shift.

4. Not fetching everything twice

The HTTP transfer cache serialises the server's GET and HEAD responses into the HTML so the client reuses them instead of repeating every request during hydration. It is on by default with provideClientHydration() and is the single biggest hydration-performance item.

Requests carrying Authorization, Proxy-Authorization or Cookie headers, and credentialed requests, are excluded — deliberately, because the cache sits in HTML a CDN may cache. That exclusion is the usual reason a team says "we turned on SSR and the client still loads everything again"; the other reasons are a client URL that differs from the server one, and POST requests, which need includePostRequests.

Tune with withHttpTransferCacheOptions({ includeHeaders, includePostRequests, filter, includeRequestsWithAuthHeaders }). Before enabling includeRequestsWithAuthHeaders, answer one question: can this route's HTML be cached by a CDN or a shared proxy? If yes, do not — you would serve one user's data to the next. withNoHttpTransferCache() disables the mechanism entirely, so every request runs twice; use it only to isolate a bug.

5. Browser-only code and mismatches

window, document, localStorage, navigator and IntersectionObserver do not exist during server rendering, and touching them at module scope or in a constructor crashes the render.

import { Component, ElementRef, PLATFORM_ID, afterNextRender, inject, signal } from '@angular/core';
import { DOCUMENT, isPlatformBrowser } from '@angular/common';

@Component({ selector: 'app-widget', template: `<div [dir]="dir()">{{ width() }}px</div>` })
export class Widget {
  private readonly el = inject(ElementRef<HTMLElement>);
  private readonly doc = inject(DOCUMENT);            // injectable — works on both platforms
  protected readonly isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
  protected readonly width = signal(0);
  protected readonly dir = signal(this.doc.documentElement.dir || 'ltr');

  constructor() {
    afterNextRender({ read: () => this.width.set(this.el.nativeElement.offsetWidth) });
  }
}

afterNextRender and afterEveryRender never run on the server, which makes them the right home for measurement and third-party widget initialisation. inject(DOCUMENT) instead of the global. isPlatformBrowser only when the branch is needed before render, and then the server branch must still produce something renderable.

Mismatch errors (NG0500/NG0502) mean the client built different DOM than the server sent. Causes, in rough order of frequency: invalid HTML the browser silently repairs (a <div> inside a <p>, a <tr> outside a <tbody>), direct DOM manipulation, Date.now()/Math.random()/crypto.randomUUID() in a template, locale- or timezone-dependent formatting, and browser extensions. Fix the cause; ngSkipHydration is containment for a third-party widget, not a fix. Full triage table, transfer-cache tuning and how to test SSR: references/hydration-debugging.md.

6. Deliverable format

## Route map — table: route | render mode | why | cache header
## Config — app.routes.server.ts, app.config.server.ts, outputMode, angular.json changes
## Hydration — provideClientHydration features (and what was left off), hydrate trigger per island
## Data — which requests the transfer cache carries, which are excluded and what that costs
## Browser-only code — every window/document access and the guard it now has
## Verify — zero hydration warnings, and the before/after LCP number

Checklist

  • Every route has an explicit renderMode; no route falls through by accident
  • getPrerenderParams is present for every prerendered parameterised route, with a chosen fallback
  • outputMode matches reality ("static" only when no route needs a server)
  • On v22, withIncrementalHydration() and withEventReplay() are absent; on v20/21 they are present
  • The console shows no NG0500/NG0502 hydration warnings on any rendered route
  • No window/document/localStorage outside afterNextRender or a platform guard
  • The transfer cache is on, any excluded request is a known accepted refetch, and no per-user data sits in a CDN-cacheable response
  • Cache-Control set per server-rendered route; personalised routes are not publicly cacheable
  • The first-load number was measured before and after

Anti-patterns

  • Turning SSR on for an internal app behind a login → nothing indexes it and there is no first-paint win; you bought a Node server to maintain.
  • withIncrementalHydration() or withEventReplay() on v22 → both redundant; provideClientHydration() already includes them.
  • ngSkipHydration to silence a mismatch → it disables hydration for that subtree, so the DOM is destroyed and rebuilt. Fix the invalid markup or the non-deterministic value instead.
  • includeRequestsWithAuthHeaders: true on a CDN-cacheable route → one user's data embedded in HTML served to the next.
  • isPlatformBrowser guards everywhere, or typeof window !== 'undefined' → most cases want afterNextRender; a guard that renders nothing on the server defeats the point of SSR, and the typeof check is untestable.
  • RenderMode.Server for content identical for every visitor → prerender it and serve it from the CDN for free.
  • Shipping SSR without a measurement → the whole point was a number; if nobody took it, the added complexity is unjustified.

Go deeper

  • references/rendering-modes.md — choosing per route, getPrerenderParams at scale, fallbacks, app shell, outputMode, CDN cache headers, server.ts and AngularNodeAppEngine
  • references/hydration-debugging.md — NG0500/NG0502 causes and fixes, transfer-cache tuning, @defer (hydrate …) selection, testing SSR
  • version-notes.md in ../angular-development/references/ — what is default in v20, v21 and v22
  • Siblings: angular-performance (measuring the result), angular-architecture (whether to render on a server at all), angular-development, angular-testing

References

Deeper material the agent loads only when needed.