Compare commits

...
2 Commits
Author SHA1 Message Date
kfj001 2ece38e52d 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>
2026-08-06 21:08:51 -07:00
robot e2356954ef feat(seo): Phase 3 — preload LCP image and reduce APP_INITIALIZER blocking
CI Pipeline / backend-test (push) Successful in 10m9s
CI Pipeline / frontend-build (push) Successful in 30s
CI Pipeline / backend-lint (push) Successful in 9s
CI Pipeline / frontend-test (push) Successful in 30s
CI Pipeline / backend-test (pull_request) Successful in 10m30s
CI Pipeline / frontend-test (pull_request) Successful in 25s
CI Pipeline / frontend-build (pull_request) Successful in 41s
CI Pipeline / backend-lint (pull_request) Successful in 9s
CI Pipeline / quality-gate (push) Successful in 3s
CI Pipeline / quality-gate (pull_request) Successful in 3s
- Add <link rel="preload"> for hero background image (LCP element) in index.html
- Remove blocking APP_INITIALIZER for config.json and station config loading
- Config loads now fire asynchronously in App.ngOnInit() (browser only)
- SSR/prerender renders instantly with default config — no network wait
- Improves LCP, Time to First Byte, and initial paint for SEO
2026-08-06 21:29:30 +00:00
5 changed files with 39 additions and 32 deletions

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 { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { authInterceptor } from './interceptors/auth.interceptor';
import {
appConfigInitProvider,
appConfigProvider,
} 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 = {
providers: [
provideBrowserGlobalErrorListeners(),
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor])),
provideRouter(routes),
appConfigProvider,
appConfigInitProvider,
stationConfigInitProvider,
],
};
+9
View File
@@ -1,5 +1,6 @@
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';
@@ -7,6 +8,7 @@ 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',
@@ -19,6 +21,7 @@ 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();
@@ -54,6 +57,12 @@ export class App implements OnInit, OnDestroy {
// These DOM operations only run in the browser.
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.
this.seoService.watchRouter();
// Handle the initial route (NavigationEnd already fired before ngOnInit).
+20 -16
View File
@@ -1,5 +1,5 @@
import { HttpClient } from '@angular/common/http';
import { APP_INITIALIZER, InjectionToken } from '@angular/core';
import { InjectionToken } from '@angular/core';
import { lastValueFrom } from 'rxjs';
import { environment } from '../../environments/environment';
@@ -16,15 +16,19 @@ const _appConfig: AppConfig = {
apiBaseUrl: environment.apiBaseUrl,
};
export function appConfigFactory(http: HttpClient) {
return (): Promise<void> => {
return lastValueFrom(http.get<AppConfig>('config.json')).then(
(config) => { Object.assign(_appConfig, config); },
() => {
// Keep defaults if config.json is not available (dev mode).
},
);
};
/**
* 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(
(config) => { Object.assign(_appConfig, config); },
() => {
// Keep defaults if config.json is not available (dev mode).
},
);
}
export const appConfigProvider = {
@@ -32,12 +36,12 @@ export const appConfigProvider = {
useValue: _appConfig,
};
export const appConfigInitProvider = {
provide: APP_INITIALIZER,
useFactory: appConfigFactory,
deps: [HttpClient],
multi: true,
};
/**
* @deprecated The APP_INITIALIZER is no longer registered by default.
* The `loadAppConfigAsync` function is called directly from `app.config.ts`
* in a non-blocking way. This export is kept for backward compatibility only.
*/
export const appConfigInitProvider = null;
export function getAppConfig(): AppConfig {
return _appConfig;
+4 -1
View File
@@ -6,7 +6,10 @@ import { StationConfig } from '../interfaces/station-config';
import { getAppConfig } from './app-config.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 = {
id: 0,
key: 'default',
+4
View File
@@ -6,6 +6,10 @@
<base href="/" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<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>
<body>
<app-root></app-root>