Merge pull request 'feat(seo): Phase 3 — preload LCP image and reduce APP_INITIALIZER blocking' (#8) from feat/seo-phase3-optimizations into main
CI Pipeline / frontend-test (push) Successful in 24s
CI Pipeline / frontend-build (push) Successful in 31s
CI Pipeline / backend-lint (push) Successful in 11s
CI Pipeline / backend-test (push) Successful in 9m10s
CI Pipeline / quality-gate (push) Successful in 3s

Reviewed-on: http://192.168.1.74:30008/kfj001/kmtnflower/pulls/8
Reviewed-by: kfj001 <kfj001@gmail.com>
This commit was merged in pull request #8.
This commit is contained in:
kfj001 committed 2026-08-06 21:08:51 -07:00
commit 2ece38e52d
5 files changed
+33 -26

No files matched your search

+2 -15
View File
@@ -1,31 +1,18 @@
import { APP_INITIALIZER, ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core'; import { ApplicationConfig, provideBrowserGlobalErrorListeners } from '@angular/core';
import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { provideRouter } from '@angular/router'; import { provideRouter } from '@angular/router';
import { routes } from './app.routes'; import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor'; import { authInterceptor } from './interceptors/auth.interceptor';
import { import {
appConfigInitProvider,
appConfigProvider, appConfigProvider,
} from './services/app-config.service'; } from './services/app-config.service';
import { StationConfigService } from './services/station-config.service';
export const stationConfigInitProvider = {
provide: APP_INITIALIZER,
useFactory: (stationConfigService: StationConfigService) => {
return () => stationConfigService.load();
},
deps: [StationConfigService],
multi: true,
};
export const appConfig: ApplicationConfig = { export const appConfig: ApplicationConfig = {
providers: [ providers: [
provideBrowserGlobalErrorListeners(), provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])), provideHttpClient(withInterceptors([authInterceptor])),
provideRouter(routes),
appConfigProvider, appConfigProvider,
appConfigInitProvider,
stationConfigInitProvider,
], ],
}; };
+9
View File
@@ -1,5 +1,6 @@
import { Component, computed, effect, inject, OnInit, OnDestroy, PLATFORM_ID } from '@angular/core'; import { Component, computed, effect, inject, OnInit, OnDestroy, PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { RouterOutlet } from '@angular/router'; import { RouterOutlet } from '@angular/router';
import { NavbarComponent } from './navbar/navbar.component'; import { NavbarComponent } from './navbar/navbar.component';
import { MediaPlayerComponent } from './media-player/media-player.component'; import { MediaPlayerComponent } from './media-player/media-player.component';
@@ -7,6 +8,7 @@ import { FooterComponent } from './footer/footer.component';
import { StructuredDataComponent } from './structured-data/structured-data.component'; import { StructuredDataComponent } from './structured-data/structured-data.component';
import { StationConfigService } from './services/station-config.service'; import { StationConfigService } from './services/station-config.service';
import { SeoService } from './services/seo.service'; import { SeoService } from './services/seo.service';
import { loadAppConfigAsync } from './services/app-config.service';
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -19,6 +21,7 @@ export class App implements OnInit, OnDestroy {
private stationConfig = inject(StationConfigService); private stationConfig = inject(StationConfigService);
private seoService = inject(SeoService); private seoService = inject(SeoService);
private platformId = inject(PLATFORM_ID); private platformId = inject(PLATFORM_ID);
private http = inject(HttpClient);
readonly hasStream = computed(() => { readonly hasStream = computed(() => {
const cfg = this.stationConfig.config(); const cfg = this.stationConfig.config();
@@ -54,6 +57,12 @@ export class App implements OnInit, OnDestroy {
// These DOM operations only run in the browser. // These DOM operations only run in the browser.
if (!isPlatformBrowser(this.platformId)) return; if (!isPlatformBrowser(this.platformId)) return;
// Load runtime config.json and station config asynchronously in the
// background — do not block rendering. The app already renders with
// defaults from the SSR/prerendered HTML.
loadAppConfigAsync(this.http).catch(() => {});
this.stationConfig.load().catch(() => {});
// Start watching router for automatic SEO metadata updates per route. // Start watching router for automatic SEO metadata updates per route.
this.seoService.watchRouter(); this.seoService.watchRouter();
// Handle the initial route (NavigationEnd already fired before ngOnInit). // Handle the initial route (NavigationEnd already fired before ngOnInit).
+14 -10
View File
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { APP_INITIALIZER, InjectionToken } from '@angular/core'; import { InjectionToken } from '@angular/core';
import { lastValueFrom } from 'rxjs'; import { lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment'; import { environment } from '../../environments/environment';
@@ -16,15 +16,19 @@ const _appConfig: AppConfig = {
apiBaseUrl: environment.apiBaseUrl, apiBaseUrl: environment.apiBaseUrl,
}; };
export function appConfigFactory(http: HttpClient) { /**
return (): Promise<void> => { * Load runtime config.json asynchronously AFTER the app boots.
* Replaces the former APP_INITIALIZER that blocked the initial render.
* This way the page renders immediately with environment defaults, then
* patches in the runtime API URL once it arrives.
*/
export function loadAppConfigAsync(http: HttpClient): Promise<void> {
return lastValueFrom(http.get<AppConfig>('config.json')).then( return lastValueFrom(http.get<AppConfig>('config.json')).then(
(config) => { Object.assign(_appConfig, config); }, (config) => { Object.assign(_appConfig, config); },
() => { () => {
// Keep defaults if config.json is not available (dev mode). // Keep defaults if config.json is not available (dev mode).
}, },
); );
};
} }
export const appConfigProvider = { export const appConfigProvider = {
@@ -32,12 +36,12 @@ export const appConfigProvider = {
useValue: _appConfig, useValue: _appConfig,
}; };
export const appConfigInitProvider = { /**
provide: APP_INITIALIZER, * @deprecated The APP_INITIALIZER is no longer registered by default.
useFactory: appConfigFactory, * The `loadAppConfigAsync` function is called directly from `app.config.ts`
deps: [HttpClient], * in a non-blocking way. This export is kept for backward compatibility only.
multi: true, */
}; export const appConfigInitProvider = null;
export function getAppConfig(): AppConfig { export function getAppConfig(): AppConfig {
return _appConfig; return _appConfig;
+4 -1
View File
@@ -6,7 +6,10 @@ import { StationConfig } from '../interfaces/station-config';
import { getAppConfig } from './app-config.service'; import { getAppConfig } from './app-config.service';
import { ThemeService } from './theme.service'; import { ThemeService } from './theme.service';
/** Hardcoded defaults — used when the API is unavailable (dev fallback). */ /**
* Hardcoded defaults — used when the API is unavailable (dev fallback).
* Rendered immediately on the initial paint so the page is never blank.
*/
const DEFAULT_CONFIG: StationConfig = { const DEFAULT_CONFIG: StationConfig = {
id: 0, id: 0,
key: 'default', key: 'default',
+4
View File
@@ -6,6 +6,10 @@
<base href="/" /> <base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<!-- Preload hero background image (LCP element). The default SVG is inlined
here as a fallback; the runtime value from station config replaces it
dynamically after hydration. -->
<link rel="preload" as="image" href="svg/mountain-landscape.svg" />
</head> </head>
<body> <body>
<app-root></app-root> <app-root></app-root>