fix(front-end): seo improvements with SSR
Some checks failed
Build and Deploy / build-and-push (push) Has been cancelled
Build and Deploy / deploy (push) Has been cancelled
Build and Deploy / test-frontend (push) Has been cancelled
Build and Deploy / test-backend (push) Has been cancelled
PR Checks / prettier-autofix (pull_request) Failing after 13s
PR Checks / security-sast (pull_request) Successful in 30s
PR Checks / test-backend (pull_request) Successful in 27s
PR Checks / test-frontend (pull_request) Successful in 1m6s

This commit is contained in:
2026-03-14 15:13:54 +01:00
parent 576380e9a0
commit ba49463ee7
4 changed files with 96 additions and 40 deletions

View File

@@ -1,47 +1,11 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject, REQUEST } from '@angular/core';
type RequestLike = {
protocol?: string;
get?: (name: string) => string | undefined;
headers?: Record<string, string | string[] | undefined>;
};
import { RequestLike, resolveRequestOrigin } from '../../../core/request-origin';
function isAbsoluteUrl(url: string): boolean {
return /^[a-z][a-z\d+\-.]*:/i.test(url) || url.startsWith('//');
}
function firstHeaderValue(value: string | string[] | undefined): string | null {
if (Array.isArray(value)) {
return value[0] ?? null;
}
return typeof value === 'string' ? value : null;
}
function resolveOrigin(request: RequestLike | null): string | null {
if (!request) {
return null;
}
const host =
request.get?.('host') ??
firstHeaderValue(request.headers?.['host']) ??
firstHeaderValue(request.headers?.['x-forwarded-host']);
if (!host) {
return null;
}
const forwardedProtoRaw = firstHeaderValue(
request.headers?.['x-forwarded-proto'],
);
const forwardedProto = forwardedProtoRaw
?.split(',')
.map((part) => part.trim().toLowerCase())
.find(Boolean);
const protocol = forwardedProto || request.protocol || 'http';
return `${protocol}://${host}`;
}
function normalizeRelativePath(url: string): string {
const withoutDot = url.replace(/^\.\//, '');
return withoutDot.startsWith('/') ? withoutDot : `/${withoutDot}`;
@@ -53,7 +17,7 @@ export const serverOriginInterceptor: HttpInterceptorFn = (req, next) => {
}
const request = inject(REQUEST, { optional: true }) as RequestLike | null;
const origin = resolveOrigin(request);
const origin = resolveRequestOrigin(request);
if (!origin) {
return next(req);
}

View File

@@ -0,0 +1,40 @@
import { resolveRequestOrigin } from './request-origin';
describe('resolveRequestOrigin', () => {
it('prefers forwarded host and protocol when present', () => {
expect(
resolveRequestOrigin({
protocol: 'http',
headers: {
host: 'internal:4000',
'x-forwarded-host': '3d-fab.ch',
'x-forwarded-proto': 'https',
},
}),
).toBe('https://3d-fab.ch');
});
it('falls back to request protocol and host', () => {
expect(
resolveRequestOrigin({
protocol: 'http',
headers: {
host: 'localhost:4000',
},
}),
).toBe('http://localhost:4000');
});
it('uses the first forwarded value when proxies append multiple entries', () => {
expect(
resolveRequestOrigin({
protocol: 'http',
headers: {
host: 'internal:4000',
'x-forwarded-host': '3d-fab.ch, proxy.local',
'x-forwarded-proto': 'https, http',
},
}),
).toBe('https://3d-fab.ch');
});
});

View File

@@ -0,0 +1,50 @@
export type RequestLike = {
protocol?: string;
get?: (name: string) => string | undefined;
headers?: Record<string, string | string[] | undefined>;
};
function firstHeaderValue(
value: string | string[] | undefined,
): string | null {
if (Array.isArray(value)) {
return value[0] ?? null;
}
return typeof value === 'string' ? value : null;
}
function firstForwardedValue(
value: string | string[] | undefined,
): string | null {
const raw = firstHeaderValue(value);
if (!raw) {
return null;
}
return (
raw
.split(',')
.map((part) => part.trim())
.find(Boolean) ?? null
);
}
export function resolveRequestOrigin(request: RequestLike | null): string | null {
if (!request) {
return null;
}
const host =
firstForwardedValue(request.headers?.['x-forwarded-host']) ??
request.get?.('host') ??
firstHeaderValue(request.headers?.['host']);
if (!host) {
return null;
}
const forwardedProto = firstForwardedValue(
request.headers?.['x-forwarded-proto'],
)?.toLowerCase();
const protocol = forwardedProto || request.protocol || 'http';
return `${protocol}://${host}`;
}

View File

@@ -4,6 +4,7 @@ import express from 'express';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import bootstrap from './main.server';
import { resolveRequestOrigin } from './core/request-origin';
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, '../browser');
@@ -39,13 +40,14 @@ app.get(
* Handle all other requests by rendering the Angular application.
*/
app.get('**', (req, res, next) => {
const { protocol, originalUrl, baseUrl, headers } = req;
const { originalUrl, baseUrl } = req;
const origin = resolveRequestOrigin(req);
commonEngine
.render({
bootstrap,
documentFilePath: indexHtml,
url: `${protocol}://${headers.host}${originalUrl}`,
url: `${origin ?? 'http://localhost:4000'}${originalUrl}`,
publicPath: browserDistFolder,
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
})