fix(ssr): inject fresh station config into CSR shell to eliminate flash
CI Pipeline / backend-test (push) Successful in 18m48s
CI Pipeline / frontend-test (push) Successful in 24s
CI Pipeline / frontend-build (push) Successful in 32s
CI Pipeline / backend-lint (push) Successful in 10s
CI Pipeline / backend-test (pull_request) Successful in 20m7s
CI Pipeline / frontend-test (pull_request) Successful in 24s
CI Pipeline / frontend-build (pull_request) Successful in 32s
CI Pipeline / backend-lint (pull_request) Successful in 10s
CI Pipeline / quality-gate (push) Successful in 3s
CI Pipeline / quality-gate (pull_request) Successful in 4s

The CSR shell previously rendered with hardcoded DEFAULT_CONFIG values,
then the client-side API call replaced them 50-500ms later, causing a
visible 'flash of stale content' (wrong colors, wrong station name).

Fix: SSR server fetches live station config from the backend API before
serving the CSR shell, injecting it as:
- Inline <script> seeding window.__STATION_CONFIG (client reads sync)
- Inline <style> setting CSS custom properties (correct colors first paint)

Also adds no-cache Cache-Control headers on HTML page responses to
prevent browsers from caching stale content.

StationConfigService now reads window.__STATION_CONFIG on construction
instead of always starting with DEFAULT_CONFIG.
This commit is contained in:
robot committed 2026-08-09 00:53:50 +00:00
1 parent 9561339cf5
commit e44f27d1cd
4 files changed
+221 -22

No files matched your search

+51 -4
View File
@@ -28,7 +28,9 @@ vi.mock('node:path', () => ({
}));
// Mock the engine manifest that only exists after ng build
vi.mock('./angular-app-engine-manifest.mjs', () => ({}));
vi.mock('./angular-app-engine-manifest.mjs', () => ({
default: {},
}));
describe('SSR server — server.ts', () => {
it('should export a request handler', async () => {
@@ -36,12 +38,12 @@ describe('SSR server — server.ts', () => {
expect(mod.requestHandler).toBeDefined();
});
it('should have spider detection imports and serveStaticShell', async () => {
it('should have spider detection imports and serveCsrShell', async () => {
const fs = require('fs');
const content = fs.readFileSync('./server.ts', 'utf-8');
expect(content).toContain('isSpider');
expect(content).toContain('getUserAgent');
expect(content).toContain('serveStaticShell');
expect(content).toContain('serveCsrShell');
expect(content).toContain('spider_detection');
});
@@ -50,7 +52,45 @@ describe('SSR server — server.ts', () => {
const content = fs.readFileSync('./server.ts', 'utf-8');
// Verify the spider detection gate exists
expect(content).toContain("if (!isSpider(userAgent))");
expect(content).toContain('serveStaticShell(res)');
expect(content).toContain('serveCsrShell(res)');
});
it('should inject fresh station config as window.__STATION_CONFIG', async () => {
const fs = require('fs');
const content = fs.readFileSync('./server.ts', 'utf-8');
expect(content).toContain('window.__STATION_CONFIG');
expect(content).toContain('buildConfigScript');
expect(content).toContain('fetchStationConfig');
});
it('should inject inline theme CSS custom properties', async () => {
const fs = require('fs');
const content = fs.readFileSync('./server.ts', 'utf-8');
expect(content).toContain('buildThemeStyle');
expect(content).toContain('COLOR_MAP');
expect(content).toContain('--color-primary');
expect(content).toContain(':root{');
});
it('should serveCsrShell be async and accept (res)', async () => {
const fs = require('fs');
const content = fs.readFileSync('./server.ts', 'utf-8');
expect(content).toMatch(/async function serveCsrShell\s*\(\s*res/);
});
it('should fetch station config from SSR_API_BASE_URL with 2s timeout', async () => {
const fs = require('fs');
const content = fs.readFileSync('./server.ts', 'utf-8');
expect(content).toContain('SSR_API_BASE_URL');
expect(content).toContain('api/station-config');
expect(content).toContain('setTimeout(2000');
});
it('should set no-cache headers on CSR shell responses', async () => {
const fs = require('fs');
const content = fs.readFileSync('./server.ts', 'utf-8');
expect(content).toContain("Cache-Control");
expect(content).toContain("no-cache, no-store, must-revalidate");
});
});
@@ -274,6 +314,13 @@ describe('nginx.conf.template — SSR proxy chain', () => {
expect(content).toContain('location /api/');
expect(content).toContain('__API_UPSTREAM__');
});
it('should add Cache-Control no-cache headers on HTML page responses', () => {
const fs = require('fs');
const content = fs.readFileSync('./nginx.conf.template', 'utf-8');
expect(content).toContain('add_header Cache-Control "no-cache, no-store, must-revalidate" always');
expect(content).toContain('add_header Pragma "no-cache" always');
});
});
describe('main.server.ts — SSR API base URL injection', () => {