CI Pipeline / backend-test (push) Successful in 5m16s
CI Pipeline / backend-test (pull_request) Successful in 5m5s
CI Pipeline / frontend-test (pull_request) Successful in 27s
CI Pipeline / frontend-build (pull_request) Failing after 29s
CI Pipeline / backend-lint (pull_request) Successful in 8s
CI Pipeline / quality-gate (push) Successful in 2s
CI Pipeline / frontend-test (push) Successful in 28s
CI Pipeline / frontend-build (push) Failing after 29s
CI Pipeline / backend-lint (push) Successful in 8s
CI Pipeline / quality-gate (pull_request) Successful in 2s
Add SeoService for dynamic metadata management: - Per-route document titles (e.g., 'KMountain Flower Radio — About') - Meta descriptions from station config per route - Canonical URLs, Open Graph tags, Twitter Card tags - Input sanitization (HTML stripping, URL validation, attribute escaping) - noindex robots directives for /login and /admin routes - Centralized router watcher (zero per-component boilerplate) Add StructuredDataService + StructuredDataComponent for JSON-LD: - Organization schema (NCBOrganization) site-wide with contact info - WebSite schema with publisher info - WebPage schema per-route with breadcrumbs - Dynamic Event schema on /events page - RadioSeason schema on /schedule page Wire up SeoService.watchRouter() in app-root ngOnInit. Wire up StructuredDataComponent in app-root template. Wire up EventsComponent and ScheduleComponent to feed data to StructuredDataService for dynamic schema injection. Includes full test suite for SeoService (22 tests).
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
import { Component, computed, effect, inject, OnInit, OnDestroy } from '@angular/core';
|
|
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';
|
|
|
|
@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);
|
|
|
|
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() {
|
|
// 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 {
|
|
// 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 {
|
|
this.resizeObserver?.disconnect();
|
|
}
|
|
|
|
private updateNavbarHeight(): void {
|
|
if (!this.navbarEl) return;
|
|
const h = this.navbarEl.offsetHeight;
|
|
document.documentElement.style.setProperty('--navbar-height', `${h}px`);
|
|
}
|
|
}
|