CI Pipeline / backend-test (push) Successful in 5m18s
CI Pipeline / frontend-test (push) Successful in 23s
CI Pipeline / frontend-build (push) Successful in 29s
CI Pipeline / backend-lint (push) Successful in 7s
CI Pipeline / quality-gate (push) Successful in 2s
CI Pipeline / quality-gate (pull_request) Successful in 2s
CI Pipeline / backend-test (pull_request) Successful in 5m14s
CI Pipeline / frontend-test (pull_request) Successful in 22s
CI Pipeline / frontend-build (pull_request) Successful in 29s
CI Pipeline / backend-lint (pull_request) Successful in 8s
The isPlatformBrowser guard in App.ngOnInit() was blocking stationConfig.load() on the server, so SSR always rendered with DEFAULT_CONFIG (hardcoded colors, assets, content). Two-part fix: - main.server.ts: Add APP_INITIALIZER that blocks server rendering until station config is fetched from the API. Only runs on the server — is a no-op in the browser to avoid blocking the client. - app.ts: Move stationConfig.load() before the isPlatformBrowser guard so ngOnInit also calls it on the server as a safety net. This ensures SSR HTML contains real DB values (colors, hero icon, station name, etc.) instead of build-time defaults — eliminating the stale-asset flash reported by users.
98 lines
3.4 KiB
TypeScript
98 lines
3.4 KiB
TypeScript
import { Component, computed, effect, inject, OnInit, OnDestroy, PLATFORM_ID } from '@angular/core';
|
|
import { isPlatformBrowser } from '@angular/common';
|
|
import { HttpClient } from '@angular/common/http';
|
|
import { RouterOutlet } from '@angular/router';
|
|
import { NavbarComponent } from './navbar/navbar.component';
|
|
import { MediaPlayerComponent } from './media-player/media-player.component';
|
|
import { FooterComponent } from './footer/footer.component';
|
|
import { StructuredDataComponent } from './structured-data/structured-data.component';
|
|
import { StationConfigService } from './services/station-config.service';
|
|
import { SeoService } from './services/seo.service';
|
|
import { loadAppConfigAsync } from './services/app-config.service';
|
|
|
|
@Component({
|
|
selector: 'app-root',
|
|
standalone: true,
|
|
imports: [RouterOutlet, NavbarComponent, MediaPlayerComponent, FooterComponent, StructuredDataComponent],
|
|
templateUrl: './app.html',
|
|
styleUrl: './app.scss',
|
|
})
|
|
export class App implements OnInit, OnDestroy {
|
|
private stationConfig = inject(StationConfigService);
|
|
private seoService = inject(SeoService);
|
|
private platformId = inject(PLATFORM_ID);
|
|
private http = inject(HttpClient);
|
|
|
|
readonly hasStream = computed(() => {
|
|
const cfg = this.stationConfig.config();
|
|
return !!cfg.stream_url && cfg.stream_url.trim().length > 0;
|
|
});
|
|
|
|
private navbarEl: HTMLElement | null = null;
|
|
private resizeObserver: ResizeObserver | null = null;
|
|
|
|
constructor() {
|
|
// These DOM operations only run in the browser.
|
|
if (!isPlatformBrowser(this.platformId)) return;
|
|
|
|
// Update favicon when station config changes.
|
|
effect(() => {
|
|
const config = this.stationConfig.config();
|
|
const faviconEl = document.querySelector('link[rel="icon"]') as HTMLLinkElement | null;
|
|
if (faviconEl && config.logo_url) {
|
|
faviconEl.href = config.logo_url;
|
|
}
|
|
});
|
|
|
|
effect(() => {
|
|
if (this.hasStream()) {
|
|
document.body.classList.add('has-stream');
|
|
} else {
|
|
document.body.classList.remove('has-stream');
|
|
}
|
|
});
|
|
}
|
|
|
|
ngOnInit(): void {
|
|
// Load station config on BOTH server and browser. On the server this
|
|
// fetches fresh data from the API so SSR renders with real colors,
|
|
// assets, and content — not DEFAULT_CONFIG baked at build time.
|
|
this.stationConfig.load().catch(() => {});
|
|
|
|
// These DOM operations only run in the browser.
|
|
if (!isPlatformBrowser(this.platformId)) return;
|
|
|
|
// Load runtime config.json asynchronously in the background — do not
|
|
// block rendering.
|
|
loadAppConfigAsync(this.http).catch(() => {});
|
|
|
|
// Start watching router for automatic SEO metadata updates per route.
|
|
this.seoService.watchRouter();
|
|
// Handle the initial route (NavigationEnd already fired before ngOnInit).
|
|
this.seoService.resetToDefaults();
|
|
|
|
// Defer until the navbar is rendered in the DOM.
|
|
queueMicrotask(() => {
|
|
this.navbarEl = document.querySelector('app-navbar');
|
|
if (!this.navbarEl) return;
|
|
|
|
this.updateNavbarHeight();
|
|
|
|
this.resizeObserver = new ResizeObserver(() => this.updateNavbarHeight());
|
|
this.resizeObserver.observe(this.navbarEl!);
|
|
});
|
|
}
|
|
|
|
ngOnDestroy(): void {
|
|
if (isPlatformBrowser(this.platformId)) {
|
|
this.resizeObserver?.disconnect();
|
|
}
|
|
}
|
|
|
|
private updateNavbarHeight(): void {
|
|
if (!this.navbarEl) return;
|
|
const h = this.navbarEl.offsetHeight;
|
|
document.documentElement.style.setProperty('--navbar-height', `${h}px`);
|
|
}
|
|
}
|