import { AngularNodeAppEngine, createNodeRequestHandler, isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; import * as fs from 'node:fs'; import * as http from 'node:http'; import * as path from 'node:path'; import { isSpider, getUserAgent } from './spider_detection'; /** * Manually import and register the Angular app engine manifest. * The Angular 21 builder generates angular-app-engine-manifest.mjs at build time * but does NOT automatically inject the import into server.mjs. * Without this, AngularNodeAppEngine fails with "Angular app engine manifest is not set" * and every SSR request returns 404. */ // @ts-ignore — generated at build time by @angular/build import engineManifest from './angular-app-engine-manifest.mjs'; import { ɵsetAngularAppEngineManifest } from '@angular/ssr'; ɵsetAngularAppEngineManifest(engineManifest); const appEngine = new AngularNodeAppEngine(); /** * Resolve the static browser index.html path. * In the built output the browser files live in the `browser/` subdirectory. */ function browserIndexPath(): string { // __dirname in ESM resolves to this file's directory const dir = typeof __dirname !== 'undefined' ? __dirname : path.dirname(new URL(import.meta.url).pathname); return path.join(dir, 'browser', 'index.html'); } /** * Serve the static CSR app shell from disk. * * Regular browser users get the prerendered HTML shell — Angular hydrates * client-side and fetches fresh config via the API. This avoids the SSR * overhead for every human request while still delivering a fast first paint. */ function serveStaticShell(res: http.ServerResponse): void { const indexPath = browserIndexPath(); try { const content = fs.readFileSync(indexPath, 'utf-8'); res.setHeader('Content-Type', 'text/html; charset=utf-8'); res.setHeader('X-Render-Mode', 'CSR'); res.end(content); } catch { res.statusCode = 500; res.end('Internal server error — index.html not found'); } } const requestHandler = createNodeRequestHandler(async (req, res, next) => { try { const response = await appEngine.handle(req); if (response) { await writeResponseToNodeResponse(response, res); } else { next(); } } catch (error) { next(error); } }); if (isMainModule(import.meta.url)) { console.log('Node.js SSR server ready'); const server = http.createServer(); server.on('request', async (req, res) => { try { // ── Spider detection gate ────────────────────────── // Only search engine spiders get full SSR rendering. // Regular users receive the static CSR app shell from disk, // which hydrates client-side and fetches fresh data from the API. const userAgent = getUserAgent(req.headers); if (!isSpider(userAgent)) { serveStaticShell(res); return; } // Spider — render full HTML via Angular SSR with live API data await requestHandler(req, res, () => { res.statusCode = 404; res.end('Not Found'); }); } catch (error) { console.error('SSR error:', error); res.statusCode = 500; res.end('Internal server error'); } }); server.listen(4200, () => { console.log('SSR server listening on http://localhost:4200'); }); } export { requestHandler };