2. Static Prerendering with Hydration, Not Runtime SSR
Status
Accepted — 2026-03-06 (pre-launch)
Context
jjk.engineer is a content-heavy blog with a fixed, buildable route manifest: posts, pages, tag indexes, the series index, and a handful of tools routes. The dynamic surface area (admin dashboards, login) is small and does not need to be crawlable or fast on first paint. Angular supports both full runtime SSR (Node server renders on request) and prerender-at-build output; the default developer instinct is to reach for SSR because it sounds more capable.
Decision
Build-time prerender with client hydration, not runtime SSR.
angular.jsonsetsprerender.discoverRoutes: falseand reads routes fromroutes.txt, which the content pipeline generates at build time from the known post/page/tag set.app.routes.server.tsdeclaresRenderMode.Prerenderas the wildcard default and explicitly downgrades/login,/admin,/admin/inbox, and/admin/ratingstoRenderMode.Client.main.server.tsandapp.config.server.tsexist only to serve the build-time prerender step. There is no Node server in the serving path; Firebase Hosting serves static HTML plus hydrated Angular bundles.
Consequences
- Zero runtime server cost for the public blog. Hosting is pure static + CDN.
- Any new public route must be addable to the build-time route manifest. Anything with per-request dynamic content (personalized SSR output) is not supported without changing this posture.
- Admin routes intentionally render client-only; they load behind auth and are not indexable. This is the correct trade for an admin surface with no SEO requirement.
- Build time scales linearly with route count. At current post volume this is not a concern, but a blog with tens of thousands of routes would eventually hit friction.
- Reversing this decision later (adopting runtime SSR) is possible — the
@angular/ssrwiring is already present — but would add a Node serving target, a cold-start profile, and an operational surface that currently does not exist.