CI Pipeline / backend-test (push) Successful in 20m50s
CI Pipeline / frontend-test (push) Successful in 24s
CI Pipeline / frontend-build (push) Successful in 32s
CI Pipeline / backend-lint (push) Successful in 15s
CI Pipeline / backend-test (pull_request) Successful in 37m43s
CI Pipeline / frontend-test (pull_request) Successful in 45s
CI Pipeline / frontend-build (pull_request) Successful in 39s
CI Pipeline / backend-lint (pull_request) Successful in 16s
CI Pipeline / quality-gate (push) Successful in 5s
CI Pipeline / quality-gate (pull_request) Successful in 4s
The CSR shell path was hardcoded to a local dev directory (app/server/browser/index.html) which does not exist in the Docker container. Browser files are copied to /usr/share/nginx/html by the Dockerfile. browserIndexPath() now checks both production and development paths, falling back to the first that exists.
125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
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.
|
|
*
|
|
* Production (Docker): browser files are copied to /usr/share/nginx/html/
|
|
* Local dev (ng build): browser files live in dist/radio-station/browser/
|
|
* Try each candidate in order and return the first that exists.
|
|
*/
|
|
function browserIndexPath(): string {
|
|
// Production path — Dockerfile copies browser output here
|
|
const productionPaths = [
|
|
'/usr/share/nginx/html/index.html',
|
|
'/var/www/html/index.html',
|
|
];
|
|
|
|
// Local dev path — relative to the compiled server directory
|
|
const dir = typeof __dirname !== 'undefined' ? __dirname : path.dirname(new URL(import.meta.url).pathname);
|
|
const localPaths = [
|
|
path.join(dir, 'browser', 'index.html'),
|
|
path.join(dir, '..', 'browser', 'index.html'),
|
|
];
|
|
|
|
const allCandidates = [...productionPaths, ...localPaths];
|
|
for (const candidate of allCandidates) {
|
|
try {
|
|
if (fs.existsSync(candidate)) return candidate;
|
|
} catch {
|
|
// ignore — keep trying
|
|
}
|
|
}
|
|
// Return the first candidate so the error path has a sensible value to log
|
|
return allCandidates[0];
|
|
}
|
|
|
|
/**
|
|
* 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 };
|