An Angular SSR build fails after adding a docs/:slug route; why, and how does getPrerenderParams let you prerender each documentation page?
answer
- catch-all Prerender meets a parameter
- list the concrete values
- string values only
- inject before the first await
basics
~20 sThe default '**' Prerender server route now matches docs/:slug, and the build cannot guess slug values, so it fails. Add a server route for docs/:slug with getPrerenderParams returning objects like { slug: 'routing' }; the build renders one page per object.
solid answer
~40 sA CLI-generated SSR app prerenders everything through `{ path: '**', renderMode: RenderMode.Prerender }`. When `docs/:slug` matches that entry, the build finds a prerendered route with a parameter and no way to fill it, and stops with an error saying `getPrerenderParams` is missing. The fix is a dedicated server route for `docs/:slug` with an async `getPrerenderParams()` that returns an array of parameter maps, such as `[{ slug: 'getting-started' }, { slug: 'routing' }]`; each map becomes one prerendered path. It runs at build time in an injection context, so `inject(DocsIndex)` works, but only before the first `await`. Every value must be a string, or the build fails. For catch-all routes the key is `'**'` and the value is a path like `'guides/forms'`. Alternatively, give the route `RenderMode.Server` or `RenderMode.Client`.
go deeper
Know that a route with a parameter needs getPrerenderParams to list its values before it can be prerendered.
Explain the default catch-all Prerender route, the shape and string-only rule of the returned objects, and the inject-before-await rule.
Decide where the slug list comes from at build time, what happens when it fails, and when a different render mode is the better fit.
Balance build duration against page count as documentation grows, and set a rule for when parameterized sections move off prerendering.
## Why the build fails A CLI-generated SSR app starts with one server route: `{ path: '**', renderMode: RenderMode.Prerender }`. During `ng build`, Angular walks the client routes and matches each one against the server routes. For `docs/:slug`: 1. The client route matches the catch-all, so its render mode is `Prerender`. 2. The path contains a parameter, so there is no single page to render. 3. No `getPrerenderParams` exists for it, so the build reports an error: the route uses prerendering and includes parameters, but `getPrerenderParams` is missing. Two ways out: tell the build which slugs exist, or choose a different render mode for the route. ## Listing the pages with getPrerenderParams `getPrerenderParams` belongs on a server route with `RenderMode.Prerender`. It returns a `Promise` of an array; each element maps parameter names to **string** values, and each element produces one page. ```ts // app.routes.server.ts import { inject } from '@angular/core'; import { RenderMode, ServerRoute } from '@angular/ssr'; import { DocsIndex } from './docs/docs-index'; export const serverRoutes: ServerRoute[] = [ { path: 'docs/:slug', renderMode: RenderMode.Prerender, async getPrerenderParams() { const docs = inject(DocsIndex); // must come before any await const slugs = await docs.listSlugs(); // e.g. ['getting-started', 'routing'] return slugs.map((slug) => ({ slug })); }, }, { path: '**', renderMode: RenderMode.Prerender }, ]; ``` The build then writes `docs/getting-started/index.html`, `docs/routing/index.html` and so on. ## The rules it must follow - **It runs at build time only.** There is no visitor and no request, and the guide warns not to rely on browser-specific or server-specific APIs for its data. - **It runs in an injection context.** `inject()` works, but only synchronously: calling it after an `await` fails, as with any injection context. - **Values must be strings.** Returning `{ id: 42 }` fails the build with a message about a non-string value for the parameter. Convert with `String(id)`. - **Every parameter needs a value.** A route `docs/:section/:slug` needs both keys in each object. - **Catch-all routes use the key `'**'`.** For `docs/**`, return `{ '**': 'guides/forms' }` to produce `docs/guides/forms`. Parameters and a catch-all can be combined, as in `product/:id/**`. - **Not every route shape is allowed.** A single-segment `*` wildcard cannot use `getPrerenderParams`, and client routes defined with a custom `matcher` cannot be prerendered at all. ## Where the list should come from The list must be available while the build runs: | Source | Fits when | |---|---| | A generated index file in the repository | Docs are authored as files next to the app | | A content API called during the build | Docs live in a separate content system that is reachable from the build machine | | A hard-coded array | The set is tiny and rarely changes | If the source is down, the build fails, which is usually the safer outcome than deploying a site with missing pages. ## Pages the list does not cover Any slug not returned is not prerendered. What happens when someone requests one is controlled by the route's `fallback` setting (`PrerenderFallback.Server` by default), and it only applies when the app is deployed with Angular's server. With a purely static deployment, unlisted slugs are whatever the static host does with an unknown path. ## Keeping the list honest The slug list and the pages must agree: - Every slug returned must be renderable at build time. If a component cannot find the document, it renders its not-found state, and that state is written to disk as a normal page. - Slugs that are removed from the source should disappear from the list, or the old pages stay deployed. - Parameter values become path segments, so they should already be URL-safe; a slug with spaces or slashes produces surprising paths. - A build log or a test that counts the generated pages catches a source that suddenly returns an empty list. ## Alternatives to prerendering the route - `RenderMode.Server` renders each slug per request. Choose it when pages change often or the set is unbounded. - `RenderMode.Client` sends the client shell. Choose it when search engines and first paint do not matter for these pages.
- Why does inject() fail when it is called after an await inside getPrerenderParams?The function runs inside an injection context that exists only for its synchronous part. After the first `await`, execution resumes in a later microtask where that context is gone, so `inject()` throws. Inject every dependency at the top of the function, then await.
- How do you prerender nested documentation paths such as docs/guides/forms?Use a catch-all server route such as `docs/**` with `getPrerenderParams` returning objects keyed by `'**'`, for example `{ '**': 'guides/forms' }`. Each value is inserted as the remaining path segments, producing `docs/guides/forms`.
saying these in an interview costs you the question
- Angular discovers slugs by crawling links from the home page.
- getPrerenderParams runs on every request to pick the right page.
- Numbers are fine as parameter values; Angular converts them.
- inject() can be called anywhere inside getPrerenderParams, even after awaits.
- Any parameterized route prerenders once with the literal :slug placeholder.