Reconfigure SSR so only search engine spiders receive full server-side rendering with fresh API data. Regular browser users get the static CSR app shell from disk.
## Summary
Reconfigure SSR so only search engine spiders receive full server-side rendering with fresh API data. Regular browser users get the static CSR app shell from disk.
Add spider detection gate to SSR server so that search engine
crawlers get full server-side rendering with fresh API data,
while regular users receive the static CSR app shell from disk.
- backend/app/spider_detection.py: Python spider detection module
- spider_detection.ts: TypeScript mirror of spider detection patterns
- server.ts: SSR gate — isSpider() check before dispatching to
Angular SSR engine; non-spiders get static index.html
- server.spec.ts: Added spider detection unit tests
- tsconfig.app.json: Include spider_detection.ts in compilation
- backend/app/models.py: Fix SQLAlchemy FK resolution (user_models import)
Breaks in deployment. Visting website as normal user gets: "Internal server error — index.html not found" (HTTP 500).. its not a good thing there are no unit tests or CI/CD build tests that capture this scenario.
Breaks in deployment. Visting website as normal user gets: "Internal server error — index.html not found" (HTTP 500).. its not a good thing there are no unit tests or CI/CD build tests that capture this scenario.
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.
Fixed the production deployment issue. Root cause: browserIndexPath() was hardcoded to app/server/browser/index.html which only exists during local development. In the Docker container, browser files are copied to /usr/share/nginx/html/ by the Dockerfile, so the CSR shell was never found for regular users, returning the HTTP 500 error.
Fix: browserIndexPath() now checks both production (/usr/share/nginx/html/index.html) and development paths, returning the first that exists.
Fixed the production deployment issue. Root cause: `browserIndexPath()` was hardcoded to `app/server/browser/index.html` which only exists during local development. In the Docker container, browser files are copied to `/usr/share/nginx/html/` by the Dockerfile, so the CSR shell was never found for regular users, returning the HTTP 500 error.
Fix: `browserIndexPath()` now checks both production (`/usr/share/nginx/html/index.html`) and development paths, returning the first that exists.
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.
Addressed the review feedback. The root cause was that the CSR shell rendered with hardcoded DEFAULT_CONFIG values, then the async API call replaced them 50-500ms later, causing the visible flash.
Changes:
server.ts — serveCsrShell() is now async. It fetches live station config from the backend API before serving the shell, injecting:
Inline <script> seeding window.__STATION_CONFIG (client reads sync on bootstrap)
Inline <style> setting CSS custom properties on :root (correct colors on first paint)
Graceful degradation: if the API fetch fails, the shell is served unmodified
Cache-Control: no-cache headers on CSR responses
Browser path resolution now checks for index.csr.html (Angular 21 SSG output)
src/app/services/station-config.service.ts — config signal now reads window.__STATION_CONFIG synchronously on construction instead of always starting with DEFAULT_CONFIG
nginx.conf.template — Added Cache-Control: no-cache, no-store, must-revalidate + Pragma: no-cache on HTML page responses to prevent browser caching of stale content
server.spec.ts — 6 new tests for config injection, theme injection, async shell serving, API timeout, no-cache headers, and nginx headers (45/45 passing)
The client now bootstraps with real data — no DEFAULT_CONFIG → real data transition window.
Addressed the review feedback. The root cause was that the CSR shell rendered with hardcoded DEFAULT_CONFIG values, then the async API call replaced them 50-500ms later, causing the visible flash.
**Changes:**
- `server.ts` — `serveCsrShell()` is now `async`. It fetches live station config from the backend API before serving the shell, injecting:
- Inline `<script>` seeding `window.__STATION_CONFIG` (client reads sync on bootstrap)
- Inline `<style>` setting CSS custom properties on `:root` (correct colors on first paint)
- Graceful degradation: if the API fetch fails, the shell is served unmodified
- `Cache-Control: no-cache` headers on CSR responses
- Browser path resolution now checks for `index.csr.html` (Angular 21 SSG output)
- `src/app/services/station-config.service.ts` — `config` signal now reads `window.__STATION_CONFIG` synchronously on construction instead of always starting with `DEFAULT_CONFIG`
- `nginx.conf.template` — Added `Cache-Control: no-cache, no-store, must-revalidate` + `Pragma: no-cache` on HTML page responses to prevent browser caching of stale content
- `server.spec.ts` — 6 new tests for config injection, theme injection, async shell serving, API timeout, no-cache headers, and nginx headers (45/45 passing)
The client now bootstraps with real data — no DEFAULT_CONFIG → real data transition window.
The browserIndexPath() function was resolving index.html first, which is
the prerendered output with stale build-time content baked into <app-root>.
Even with window.__STATION_CONFIG injection, the prerendered innerHTML
still shows old content on the first paint until Angular re-renders.
By preferring index.csr.html (the pure CSR shell with empty <app-root>),
regular users get a clean first paint — no stale prerendered DOM to flash.
Fixed. The root cause was that browserIndexPath() was resolving index.html first, which is the prerendered output with stale build-time DEFAULT_CONFIG baked into <app-root>. Even with window.__STATION_CONFIG injection, the prerendered innerHTML still showed old content on the first paint until Angular re-rendered.
Fix: Prefer index.csr.html (the pure CSR shell with empty <app-root>) over index.html. Regular users now get a clean first paint — no stale prerendered DOM to flash.
Commit: 1b38770
Fixed. The root cause was that `browserIndexPath()` was resolving `index.html` first, which is the prerendered output with stale build-time DEFAULT_CONFIG baked into `<app-root>`. Even with `window.__STATION_CONFIG` injection, the prerendered innerHTML still showed old content on the first paint until Angular re-rendered.
**Fix:** Prefer `index.csr.html` (the pure CSR shell with empty `<app-root>`) over `index.html`. Regular users now get a clean first paint — no stale prerendered DOM to flash.
Commit: `1b38770`
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Reconfigure SSR so only search engine spiders receive full server-side rendering with fresh API data. Regular browser users get the static CSR app shell from disk.
Breaks in deployment. Visting website as normal user gets: "Internal server error — index.html not found" (HTTP 500).. its not a good thing there are no unit tests or CI/CD build tests that capture this scenario.
Fixed the production deployment issue. Root cause:
browserIndexPath()was hardcoded toapp/server/browser/index.htmlwhich only exists during local development. In the Docker container, browser files are copied to/usr/share/nginx/html/by the Dockerfile, so the CSR shell was never found for regular users, returning the HTTP 500 error.Fix:
browserIndexPath()now checks both production (/usr/share/nginx/html/index.html) and development paths, returning the first that exists.This deploys! But it doesn't fix the underlying issue. Other clients are still getting SSR pages at first.
Addressed the review feedback. The root cause was that the CSR shell rendered with hardcoded DEFAULT_CONFIG values, then the async API call replaced them 50-500ms later, causing the visible flash.
Changes:
server.ts—serveCsrShell()is nowasync. It fetches live station config from the backend API before serving the shell, injecting:<script>seedingwindow.__STATION_CONFIG(client reads sync on bootstrap)<style>setting CSS custom properties on:root(correct colors on first paint)Cache-Control: no-cacheheaders on CSR responsesindex.csr.html(Angular 21 SSG output)src/app/services/station-config.service.ts—configsignal now readswindow.__STATION_CONFIGsynchronously on construction instead of always starting withDEFAULT_CONFIGnginx.conf.template— AddedCache-Control: no-cache, no-store, must-revalidate+Pragma: no-cacheon HTML page responses to prevent browser caching of stale contentserver.spec.ts— 6 new tests for config injection, theme injection, async shell serving, API timeout, no-cache headers, and nginx headers (45/45 passing)The client now bootstraps with real data — no DEFAULT_CONFIG → real data transition window.
Fixed. The root cause was that
browserIndexPath()was resolvingindex.htmlfirst, which is the prerendered output with stale build-time DEFAULT_CONFIG baked into<app-root>. Even withwindow.__STATION_CONFIGinjection, the prerendered innerHTML still showed old content on the first paint until Angular re-rendered.Fix: Prefer
index.csr.html(the pure CSR shell with empty<app-root>) overindex.html. Regular users now get a clean first paint — no stale prerendered DOM to flash.Commit:
1b38770