feat: add SSR and prerendering support #7
No files matched your search
@@ -44,6 +44,7 @@
|
|||||||
"builder": "@angular/build:application",
|
"builder": "@angular/build:application",
|
||||||
"options": {
|
"options": {
|
||||||
"browser": "src/main.ts",
|
"browser": "src/main.ts",
|
||||||
|
"server": "src/main.server.ts",
|
||||||
"tsConfig": "tsconfig.app.json",
|
"tsConfig": "tsconfig.app.json",
|
||||||
"inlineStyleLanguage": "scss",
|
"inlineStyleLanguage": "scss",
|
||||||
"assets": [
|
"assets": [
|
||||||
@@ -55,6 +56,12 @@
|
|||||||
"styles": ["src/styles.scss"],
|
"styles": ["src/styles.scss"],
|
||||||
"stylePreprocessorOptions": {
|
"stylePreprocessorOptions": {
|
||||||
"includePaths": ["src/styles"]
|
"includePaths": ["src/styles"]
|
||||||
|
},
|
||||||
|
"ssr": {
|
||||||
|
"entry": "server.ts"
|
||||||
|
},
|
||||||
|
"prerender": {
|
||||||
|
"routesFile": "prerender-routes.txt"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
|||||||
Generated
+52
-37
@@ -14,7 +14,9 @@
|
|||||||
"@angular/core": "^21.2.0",
|
"@angular/core": "^21.2.0",
|
||||||
"@angular/forms": "^21.2.0",
|
"@angular/forms": "^21.2.0",
|
||||||
"@angular/platform-browser": "^21.2.0",
|
"@angular/platform-browser": "^21.2.0",
|
||||||
|
"@angular/platform-server": "^21.2.19",
|
||||||
"@angular/router": "^21.2.0",
|
"@angular/router": "^21.2.0",
|
||||||
|
"@angular/ssr": "^21.2.20",
|
||||||
"@types/leaflet": "^1.9.17",
|
"@types/leaflet": "^1.9.17",
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
@@ -29,6 +31,7 @@
|
|||||||
"@angular/cli": "^21.2.14",
|
"@angular/cli": "^21.2.14",
|
||||||
"@angular/compiler-cli": "^21.2.0",
|
"@angular/compiler-cli": "^21.2.0",
|
||||||
"@types/jsdom": "^21.1.7",
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/node": "^26.1.2",
|
||||||
"@vitest/coverage-v8": "^4.0.8",
|
"@vitest/coverage-v8": "^4.0.8",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
@@ -603,6 +606,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/platform-server": {
|
||||||
|
"version": "21.2.19",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-21.2.19.tgz",
|
||||||
|
"integrity": "sha512-UVyUIj04LRBoi4KfvxeZYPohzh80nSqamxpb8uzsYFKVSYdZSHDiFaE6OOzj0YST1rs2/LC92cPE2TY7IQIdew==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0",
|
||||||
|
"xhr2": "^0.2.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^20.19.0 || ^22.12.0 || >=24.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "21.2.19",
|
||||||
|
"@angular/compiler": "21.2.19",
|
||||||
|
"@angular/core": "21.2.19",
|
||||||
|
"@angular/platform-browser": "21.2.19",
|
||||||
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@angular/router": {
|
"node_modules/@angular/router": {
|
||||||
"version": "21.2.16",
|
"version": "21.2.16",
|
||||||
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.16.tgz",
|
"resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.16.tgz",
|
||||||
@@ -621,6 +644,26 @@
|
|||||||
"rxjs": "^6.5.3 || ^7.4.0"
|
"rxjs": "^6.5.3 || ^7.4.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@angular/ssr": {
|
||||||
|
"version": "21.2.20",
|
||||||
|
"resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-21.2.20.tgz",
|
||||||
|
"integrity": "sha512-MZn7Hssm2EYaQ8zWGD+o2fzVs4BLGHLXSg2L7JBmAjlXhSyJeBYw2PUOzSX4gqCNF6wR8ikmJJTel0q8Bo6oow==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.3.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@angular/common": "^21.0.0",
|
||||||
|
"@angular/core": "^21.0.0",
|
||||||
|
"@angular/platform-server": "^21.0.0",
|
||||||
|
"@angular/router": "^21.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@angular/platform-server": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@asamuzakjp/css-color": {
|
"node_modules/@asamuzakjp/css-color": {
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
|
||||||
@@ -1060,43 +1103,6 @@
|
|||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@emnapi/core": {
|
|
||||||
"version": "1.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz",
|
|
||||||
"integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@emnapi/wasi-threads": "1.2.2",
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/runtime": {
|
|
||||||
"version": "1.11.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz",
|
|
||||||
"integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@emnapi/wasi-threads": {
|
|
||||||
"version": "1.2.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz",
|
|
||||||
"integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"optional": true,
|
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
|
||||||
"tslib": "^2.4.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
"version": "0.27.3",
|
"version": "0.27.3",
|
||||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz",
|
||||||
@@ -8732,6 +8738,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/xhr2": {
|
||||||
|
"version": "0.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz",
|
||||||
|
"integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/xml-name-validator": {
|
"node_modules/xml-name-validator": {
|
||||||
"version": "5.0.0",
|
"version": "5.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
|
||||||
|
|||||||
+6
-3
@@ -14,14 +14,16 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "npm@11.11.0",
|
"packageManager": "npm@11.11.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@angular/cdk": "^21.2.0",
|
||||||
"@angular/common": "^21.2.0",
|
"@angular/common": "^21.2.0",
|
||||||
"@angular/compiler": "^21.2.0",
|
"@angular/compiler": "^21.2.0",
|
||||||
"@angular/core": "^21.2.0",
|
"@angular/core": "^21.2.0",
|
||||||
"@angular/forms": "^21.2.0",
|
"@angular/forms": "^21.2.0",
|
||||||
"@angular/platform-browser": "^21.2.0",
|
"@angular/platform-browser": "^21.2.0",
|
||||||
|
"@angular/platform-server": "^21.2.19",
|
||||||
"@angular/router": "^21.2.0",
|
"@angular/router": "^21.2.0",
|
||||||
|
"@angular/ssr": "^21.2.20",
|
||||||
"@types/leaflet": "^1.9.17",
|
"@types/leaflet": "^1.9.17",
|
||||||
"@angular/cdk": "^21.2.0",
|
|
||||||
"chart.js": "^4.4.0",
|
"chart.js": "^4.4.0",
|
||||||
"leaflet": "^1.9.4",
|
"leaflet": "^1.9.4",
|
||||||
"ng2-charts": "^10.0.0",
|
"ng2-charts": "^10.0.0",
|
||||||
@@ -34,11 +36,12 @@
|
|||||||
"@angular/build": "^21.2.14",
|
"@angular/build": "^21.2.14",
|
||||||
"@angular/cli": "^21.2.14",
|
"@angular/cli": "^21.2.14",
|
||||||
"@angular/compiler-cli": "^21.2.0",
|
"@angular/compiler-cli": "^21.2.0",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
|
"@types/node": "^26.1.2",
|
||||||
"@vitest/coverage-v8": "^4.0.8",
|
"@vitest/coverage-v8": "^4.0.8",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"prettier": "^3.8.1",
|
"prettier": "^3.8.1",
|
||||||
"typescript": "~5.9.2",
|
"typescript": "~5.9.2",
|
||||||
"vitest": "^4.0.8",
|
"vitest": "^4.0.8"
|
||||||
"@types/jsdom": "^21.1.7"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
/
|
||||||
|
/about
|
||||||
|
/schedule
|
||||||
|
/events
|
||||||
|
/contact
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import {
|
||||||
|
AngularNodeAppEngine,
|
||||||
|
createNodeRequestHandler,
|
||||||
|
isMainModule,
|
||||||
|
writeResponseToNodeResponse,
|
||||||
|
} from '@angular/ssr/node';
|
||||||
|
import * as http from 'node:http';
|
||||||
|
|
||||||
|
const ngApp = new AngularNodeAppEngine();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle incoming requests using the Angular SSR engine.
|
||||||
|
* Prerendered routes are served as static HTML at build time.
|
||||||
|
* Dynamic routes fall back to server-side rendering.
|
||||||
|
*/
|
||||||
|
export default createNodeRequestHandler(async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const response = await ngApp.handle(req);
|
||||||
|
if (response) {
|
||||||
|
await writeResponseToNodeResponse(response, res);
|
||||||
|
} else {
|
||||||
|
next();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMainModule(import.meta.url)) {
|
||||||
|
const httpServer = http.createServer();
|
||||||
|
httpServer.on('request', async (req: http.IncomingMessage, res: http.ServerResponse) => {
|
||||||
|
const handler = (await import('./server.js')).default;
|
||||||
|
handler(req, res, () => {
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('Not Found');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
httpServer.listen(4200, () => {
|
||||||
|
console.log(`Node SSR server listening on http://localhost:4200`);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { RenderMode, ServerRoute } from '@angular/ssr';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Server routes configuration for Angular SSR.
|
||||||
|
*
|
||||||
|
* The five public routes are prerendered at build time (SSG) for optimal SEO.
|
||||||
|
* Authenticated routes (/login, /admin) use SSR for dynamic rendering.
|
||||||
|
* All other routes fall back to SSR.
|
||||||
|
*/
|
||||||
|
export const serverRoutes: ServerRoute[] = [
|
||||||
|
{ path: '', renderMode: RenderMode.Prerender },
|
||||||
|
{ path: 'about', renderMode: RenderMode.Prerender },
|
||||||
|
{ path: 'schedule', renderMode: RenderMode.Prerender },
|
||||||
|
{ path: 'events', renderMode: RenderMode.Prerender },
|
||||||
|
{ path: 'contact', renderMode: RenderMode.Prerender },
|
||||||
|
{ path: 'login', renderMode: RenderMode.Server },
|
||||||
|
{ path: 'admin', renderMode: RenderMode.Server },
|
||||||
|
{ path: '**', renderMode: RenderMode.Server },
|
||||||
|
];
|
||||||
+11
-1
@@ -1,4 +1,5 @@
|
|||||||
import { Component, computed, effect, inject, OnInit, OnDestroy } from '@angular/core';
|
import { Component, computed, effect, inject, OnInit, OnDestroy, PLATFORM_ID } from '@angular/core';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
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';
|
||||||
@@ -17,6 +18,7 @@ import { SeoService } from './services/seo.service';
|
|||||||
export class App implements OnInit, OnDestroy {
|
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);
|
||||||
|
|
||||||
readonly hasStream = computed(() => {
|
readonly hasStream = computed(() => {
|
||||||
const cfg = this.stationConfig.config();
|
const cfg = this.stationConfig.config();
|
||||||
@@ -27,6 +29,9 @@ export class App implements OnInit, OnDestroy {
|
|||||||
private resizeObserver: ResizeObserver | null = null;
|
private resizeObserver: ResizeObserver | null = null;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// These DOM operations only run in the browser.
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
// Update favicon when station config changes.
|
// Update favicon when station config changes.
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const config = this.stationConfig.config();
|
const config = this.stationConfig.config();
|
||||||
@@ -46,6 +51,9 @@ export class App implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// These DOM operations only run in the browser.
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
// 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).
|
||||||
@@ -64,8 +72,10 @@ export class App implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
this.resizeObserver?.disconnect();
|
this.resizeObserver?.disconnect();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private updateNavbarHeight(): void {
|
private updateNavbarHeight(): void {
|
||||||
if (!this.navbarEl) return;
|
if (!this.navbarEl) return;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
import { Event } from '../interfaces/event';
|
import { Event } from '../interfaces/event';
|
||||||
import { getAppConfig } from './app-config.service';
|
import { getAppConfig } from './app-config.service';
|
||||||
@@ -16,7 +17,12 @@ export class EventService {
|
|||||||
getEvents(active?: boolean): Observable<Event[]> {
|
getEvents(active?: boolean): Observable<Event[]> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
if (active !== undefined) params = params.set('active', String(active));
|
if (active !== undefined) params = params.set('active', String(active));
|
||||||
return this.http.get<Event[]>(this.baseUrl, { params });
|
return this.http.get<Event[]>(this.baseUrl, { params }).pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.warn('EventService: Failed to load events, returning empty array.', err);
|
||||||
|
return of<Event[]>([]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getEvent(id: number): Observable<Event> {
|
getEvent(id: number): Observable<Event> {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Injectable, inject } from '@angular/core';
|
import { Injectable, inject } from '@angular/core';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { catchError } from 'rxjs/operators';
|
||||||
|
|
||||||
import { Show } from '../interfaces/show';
|
import { Show } from '../interfaces/show';
|
||||||
import { getAppConfig } from './app-config.service';
|
import { getAppConfig } from './app-config.service';
|
||||||
@@ -41,7 +42,12 @@ export class ShowService {
|
|||||||
private baseUrl = `${getAppConfig().apiBaseUrl}/api/shows`;
|
private baseUrl = `${getAppConfig().apiBaseUrl}/api/shows`;
|
||||||
|
|
||||||
getShows(): Observable<Show[]> {
|
getShows(): Observable<Show[]> {
|
||||||
return this.http.get<Show[]>(this.baseUrl);
|
return this.http.get<Show[]>(this.baseUrl).pipe(
|
||||||
|
catchError((err) => {
|
||||||
|
console.warn('ShowService: Failed to load shows, returning empty array.', err);
|
||||||
|
return of<Show[]>([]);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
getShow(id: number): Observable<Show> {
|
getShow(id: number): Observable<Show> {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, inject, PLATFORM_ID } from '@angular/core';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
|
||||||
import { StationConfig } from '../interfaces/station-config';
|
import { StationConfig } from '../interfaces/station-config';
|
||||||
|
|
||||||
@@ -32,11 +33,16 @@ function gradient(from: string, mid: string, to: string): string {
|
|||||||
providedIn: 'root',
|
providedIn: 'root',
|
||||||
})
|
})
|
||||||
export class ThemeService {
|
export class ThemeService {
|
||||||
|
private platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply color theme from StationConfig as CSS custom properties on `:root`.
|
* Apply color theme from StationConfig as CSS custom properties on `:root`.
|
||||||
* Also generates derived gradient variables.
|
* Also generates derived gradient variables.
|
||||||
|
* Skips silently on the server (SSR) — CSS vars are set in the browser after hydration.
|
||||||
*/
|
*/
|
||||||
apply(config: StationConfig): void {
|
apply(config: StationConfig): void {
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
const root = document.documentElement.style;
|
const root = document.documentElement.style;
|
||||||
|
|
||||||
for (const { field, prop } of COLOR_MAP) {
|
for (const { field, prop } of COLOR_MAP) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Component, effect, inject } from '@angular/core';
|
import { Component, effect, inject, PLATFORM_ID, OnDestroy } from '@angular/core';
|
||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
import { StructuredDataService } from '../services/structured-data.service';
|
import { StructuredDataService } from '../services/structured-data.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -16,10 +17,14 @@ import { StructuredDataService } from '../services/structured-data.service';
|
|||||||
standalone: true,
|
standalone: true,
|
||||||
template: '', // renders nothing in the body — writes to <head>
|
template: '', // renders nothing in the body — writes to <head>
|
||||||
})
|
})
|
||||||
export class StructuredDataComponent {
|
export class StructuredDataComponent implements OnDestroy {
|
||||||
private service = inject(StructuredDataService);
|
private service = inject(StructuredDataService);
|
||||||
|
private platformId = inject(PLATFORM_ID);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// Only manipulate DOM in the browser; skip on the server.
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
|
|
||||||
effect(() => {
|
effect(() => {
|
||||||
const entries = this.service.structuredData();
|
const entries = this.service.structuredData();
|
||||||
this.renderEntries(entries);
|
this.renderEntries(entries);
|
||||||
@@ -27,6 +32,7 @@ export class StructuredDataComponent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
|
if (!isPlatformBrowser(this.platformId)) return;
|
||||||
this.clearTags();
|
this.clearTags();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import { APP_ID } from '@angular/core';
|
||||||
|
import { bootstrapApplication, BootstrapContext } from '@angular/platform-browser';
|
||||||
|
import { provideServerRendering, withRoutes } from '@angular/ssr';
|
||||||
|
|
||||||
|
import { appConfig } from './app/app.config';
|
||||||
|
import { App } from './app/app';
|
||||||
|
import { serverRoutes } from './app/app.server.routes';
|
||||||
|
|
||||||
|
const serverAppConfig = {
|
||||||
|
...appConfig,
|
||||||
|
providers: [
|
||||||
|
...appConfig.providers,
|
||||||
|
{ provide: APP_ID, useValue: 'server' },
|
||||||
|
provideServerRendering(withRoutes(serverRoutes)),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bootstrap function for server-side rendering.
|
||||||
|
* Exports a function that accepts BootstrapContext and returns ApplicationRef,
|
||||||
|
* as required by @angular/ssr's CommonEngine bootstrap signature.
|
||||||
|
*/
|
||||||
|
export default (ctx: BootstrapContext) =>
|
||||||
|
bootstrapApplication(App, serverAppConfig, ctx);
|
||||||
Reference in new issue
Block a user