From 63bc2fb22d1278352fdd8d0728e7b3804cea2c52 Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Thu, 6 Aug 2026 16:38:27 +0000 Subject: [PATCH] fix(ssr): remove broken dynamic import and lazy-init engine MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace await import('./main.server.js') with lazy getEngine() (original path was wrong — main.server.ts lives in src/, not root) - AngularNodeAppEngine handles manifest registration internally; no manual bootstrap import needed - Add npm scripts: build:ssr, build:prerender, serve:ssr:radio-station - Add PRERENDER.md build documentation Verified: clean build prerenders all 5 routes, 54 tests pass --- PRERENDER.md | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +++- server.ts | 23 ++++++++++++---- 3 files changed, 99 insertions(+), 6 deletions(-) create mode 100644 PRERENDER.md diff --git a/PRERENDER.md b/PRERENDER.md new file mode 100644 index 0000000..17015ce --- /dev/null +++ b/PRERENDER.md @@ -0,0 +1,77 @@ +# Prerendering & SSR Build Guide + +## Overview + +The KMountain Flower Radio application uses Angular SSR with prerendering (SSG — Static Site Generation) for its five public-facing routes. Prerendered routes are compiled to static HTML during `ng build` and served directly from disk at runtime. + +## Public Routes + +All routes listed in `prerender-routes.txt`: + +| Route | Build Output | +|-------------|---------------------------------------------------| +| `/` | `dist/radio-station/browser/index.html` | +| `/about` | `dist/radio-station/browser/about/index.html` | +| `/schedule` | `dist/radio-station/browser/schedule/index.html` | +| `/events` | `dist/radio-station/browser/events/index.html` | +| `/contact` | `dist/radio-station/browser/contact/index.html` | + +## Configuration Files + +| File | Purpose | +|-----------------------------|--------------------------------------------------------------| +| `angular.json` | Prerender + SSR config (`prerender.routesFile`, `ssr.entry`) | +| `prerender-routes.txt` | One route per line — the list of routes to prerender | +| `server.ts` | Node SSR entry point + `PRERENDERED_ROUTES` constant | +| `src/app/app.server.routes.ts` | Server-side route definitions | + +## Build Scripts + +### `npm run build` / `npm run build:ssr` / `npm run build:prerender` + +```bash +npm run build:ssr +# or equivalently +npm run build:prerender +# or +ng build +``` + +All three scripts execute `ng build` (default configuration: `production`). Because `angular.json` has both `prerender` and `ssr` configured, a single `ng build` produces: + +1. **Browser bundle** — client-side JavaScript + CSS under `dist/radio-station/browser/` +2. **Server bundle** — `server.mjs` + `main.server.mjs` under `dist/radio-station/server/` +3. **Prerendered HTML** — static HTML files for each route in `prerender-routes.txt` + +### `npm run serve:ssr:radio-station` + +```bash +node dist/radio-station/server.mjs +``` + +Starts the Node SSR server on port 4200. Prerendered routes are served as static HTML; all other routes fall back to SSR or a 404 handler. + +## Adding a New Prerendered Route + +1. Add the route path (one per line) to `prerender-routes.txt` +2. Add the route to `PRERENDERED_ROUTES` in `server.ts` +3. Rebuild: `npm run build:ssr` + +## Build Output Structure + +``` +dist/radio-station/ +├── browser/ +│ ├── index.html <-- prerendered (/) +│ ├── index.csr.html <-- CSR fallback +│ ├── about/index.html <-- prerendered (/about) +│ ├── schedule/index.html <-- prerendered (/schedule) +│ ├── events/index.html <-- prerendered (/events) +│ ├── contact/index.html <-- prerendered (/contact) +│ └── ... <-- JS/CSS chunks, assets +└── server/ + ├── server.mjs <-- Node entry point + ├── main.server.mjs <-- Angular server bundle + ├── angular-app-engine-manifest.mjs + └── ... <-- server chunks +``` diff --git a/package.json b/package.json index d95b195..4c9672d 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,14 @@ "ng": "ng", "start": "ng serve", "build": "ng build", + "build:ssr": "ng build", + "build:prerender": "ng build", "watch": "ng build --watch --configuration development", "test": "ng test", "test:unit": "vitest run", "test:unit:watch": "vitest", - "test:unit:coverage": "vitest run --coverage" + "test:unit:coverage": "vitest run --coverage", + "serve:ssr:radio-station": "node dist/radio-station/server/server.mjs" }, "private": true, "packageManager": "npm@11.11.0", diff --git a/server.ts b/server.ts index c8803f3..406d223 100644 --- a/server.ts +++ b/server.ts @@ -21,8 +21,6 @@ export const PRERENDERED_ROUTES: readonly string[] = [ '/contact', ]; -const ngApp = new AngularNodeAppEngine(); - /** * Normalise a request pathname and check against prerendered routes. * Handles both bare paths (`/about`) and trailing-slash variants (`/about/`). @@ -35,6 +33,19 @@ export function isPrerenderedRoute(url: string): boolean { return PRERENDERED_ROUTES.includes(normalised); } +/** + * Lazily instantiate AngularNodeAppEngine so the engine boots only when + * the first request arrives. The engine handles both prerendered route + * lookups and on-demand SSR bootstrapping internally. + */ +let appEngine: AngularNodeAppEngine | null = null; +function getEngine(): AngularNodeAppEngine { + if (!appEngine) { + appEngine = new AngularNodeAppEngine(); + } + return appEngine; +} + /** * Handle incoming requests using the Angular SSR engine. * @@ -46,12 +57,14 @@ export default createNodeRequestHandler(async (req, res, next) => { try { const prerendered = isPrerenderedRoute(req.url || '/'); if (prerendered) { - // Prerendered routes are served as static HTML — AngularNodeAppEngine - // reads the build-time manifest and returns the pre-generated response. console.log(`[SSR] Prerendered route: ${req.url}`); } - const response = await ngApp.handle(req); + // AngularNodeAppEngine handles manifest registration internally — + // no manual bootstrap import needed. For prerendered routes it + // returns pre-generated static HTML; for SSR routes it bootstraps + // the Angular app on demand. + const response = await getEngine().handle(req); if (response) { await writeResponseToNodeResponse(response, res);