Org Skills
Referenceangular-ssr

Choosing a render mode per route

Supporting material for angular-ssr. Agents load it on demand; it ships inside the skill folder.

RawSource

Open this when mapping routes to render modes, prerendering a large parameterised route, or deciding cache headers and deployment shape. Verified 2026-09 against https://angular.dev/guide/ssr.

1. The decision, route by route

The content is… Mode Why
Identical for everyone, changes on deploy RenderMode.Prerender Built once, served from a CDN, zero server cost, fastest possible TTFB
Identical for everyone but changes more often than you deploy RenderMode.Server + a short Cache-Control The CDN still does most of the work; the server refreshes it
Depends on the request (query, geo, A/B, locale from a header) RenderMode.Server Cannot be known at build time
Personalised or behind authentication RenderMode.Client Nothing to index, and server-rendering per user costs money for no gain
An app shell around a client-rendered area RenderMode.Client + withAppShell(AppShell) The shell is prerendered so first paint is instant; the route fills in

Write the table for your app before touching the config. The common mistake is one mode for the whole app: a marketing site with a logged-in dashboard wants all three.

2. getPrerenderParams

{
  path: 'products/:category/:slug',
  renderMode: RenderMode.Prerender,
  async getPrerenderParams() {
    const products: { category: string; slug: string }[] =
      await fetch('https://api.example.com/products?fields=category,slug').then((r) => r.json());
    return products.map(({ category, slug }) => ({ category, slug }));
  },
}
  • Returns one object per generated page, with a key for every parameter in the path. A missing key means that page is not generated.
  • It runs at build time, so the build now depends on that API being up. Cache the response in CI, or fail the build loudly rather than shipping a site missing half its pages.
  • Ten thousand products means ten thousand HTML files and a long build. Above roughly a thousand pages, prerender the top slice by traffic and let fallback: PrerenderFallback.Server render the long tail on demand.
  • A catch-all path: '**' cannot be prerendered; give it RenderMode.Server or RenderMode.Client.

fallback decides what happens to a path the params did not cover:

Value Behaviour Use when
PrerenderFallback.Server (default) Render on demand on the server A long tail exists and should still be indexable
PrerenderFallback.Client Ship the shell, fetch on the client The long tail does not need SEO
PrerenderFallback.None 404 The prerendered set is exhaustive by definition

3. Headers and status per route

{
  path: 'articles/:slug',
  renderMode: RenderMode.Server,
  headers: {
    'Cache-Control': 'public, max-age=60, stale-while-revalidate=600',
    'X-Content-Type-Options': 'nosniff',
  },
},
{ path: 'gone/:id', renderMode: RenderMode.Server, status: 410 },

stale-while-revalidate is what makes server rendering cheap: the CDN keeps serving the old HTML while it refreshes in the background, so a traffic spike hits the CDN rather than your Node process.

Never make a personalised server-rendered route publicly cacheable. Cache-Control: private, no-store on anything that varies per user. The transfer cache embeds fetched data in that HTML, so a shared cache entry leaks it — this is the highest-severity mistake in this whole area.

4. outputMode

// angular.json → architect → build → options
"outputMode": "server"
  • "server" (the default): a Node server handles requests; prerendered routes are still served as static files from dist/<app>/browser.
  • "static": no server is produced. Only valid when every route is Prerender or Client. This is the cheapest deployment there is — object storage plus a CDN — and worth checking for before accepting a server.

5. server.ts

ng add @angular/ssr generates it; you edit it only to add your own middleware.

import { AngularNodeAppEngine, writeResponseToNodeResponse } from '@angular/ssr/node';
import express from 'express';

const app = express();
const angularApp = new AngularNodeAppEngine();

app.use('*', (req, res, next) => {
  angularApp
    .handle(req)
    .then((response) => (response ? writeResponseToNodeResponse(response, res) : next()))
    .catch(next);
});

Things that belong here: health checks, security headers, a request-id, compression, a reverse-proxy trust setting. Things that do not: business logic, database access, or an API — those belong in your actual backend. An SSR server that grows endpoints becomes a second backend nobody planned to own.

For other runtimes there are equivalents (AngularAppEngine for edge/web-standard request handling); the shape is identical.

6. Deployment shape

Mode Deploy to Watch
"static" Object storage + CDN Rebuild and redeploy whenever prerendered content changes
"server" Node container or a managed Node host, behind a CDN Memory per instance, cold starts, and a CDN configured to respect your per-route Cache-Control

Server rendering adds an always-on process to your operational surface: it can be down, it can be slow, it can leak memory, and it needs the same logging, alerting and deploy pipeline as any other service. If the only reason for it is a marketing page, prerender that page and keep the app client-rendered.

7. When SSR is the wrong answer

  • Behind a login. Nothing indexes it; the user already waited for auth; you gained a server and lost simplicity.
  • An internal tool. Nobody is on a cold 3G connection looking at your admin panel.
  • "For performance", with no measurement. SSR improves first contentful paint and LCP; it does nothing for interaction latency, and a poorly configured transfer cache can make the total loading work worse.
  • To fix a slow API. The server calls the same API. Server rendering moves the wait, it does not remove it — and now the user sees a blank browser tab rather than a skeleton.