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
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:
@@ -1,47 +1,11 @@
|
|||||||
import { HttpInterceptorFn } from '@angular/common/http';
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
import { inject, REQUEST } from '@angular/core';
|
import { inject, REQUEST } from '@angular/core';
|
||||||
|
import { RequestLike, resolveRequestOrigin } from '../../../core/request-origin';
|
||||||
type RequestLike = {
|
|
||||||
protocol?: string;
|
|
||||||
get?: (name: string) => string | undefined;
|
|
||||||
headers?: Record<string, string | string[] | undefined>;
|
|
||||||
};
|
|
||||||
|
|
||||||
function isAbsoluteUrl(url: string): boolean {
|
function isAbsoluteUrl(url: string): boolean {
|
||||||
return /^[a-z][a-z\d+\-.]*:/i.test(url) || url.startsWith('//');
|
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 {
|
function normalizeRelativePath(url: string): string {
|
||||||
const withoutDot = url.replace(/^\.\//, '');
|
const withoutDot = url.replace(/^\.\//, '');
|
||||||
return withoutDot.startsWith('/') ? withoutDot : `/${withoutDot}`;
|
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 request = inject(REQUEST, { optional: true }) as RequestLike | null;
|
||||||
const origin = resolveOrigin(request);
|
const origin = resolveRequestOrigin(request);
|
||||||
if (!origin) {
|
if (!origin) {
|
||||||
return next(req);
|
return next(req);
|
||||||
}
|
}
|
||||||
|
|||||||
40
frontend/src/core/request-origin.spec.ts
Normal file
40
frontend/src/core/request-origin.spec.ts
Normal 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');
|
||||||
|
});
|
||||||
|
});
|
||||||
50
frontend/src/core/request-origin.ts
Normal file
50
frontend/src/core/request-origin.ts
Normal 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}`;
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import express from 'express';
|
|||||||
import { dirname, join, resolve } from 'node:path';
|
import { dirname, join, resolve } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import bootstrap from './main.server';
|
import bootstrap from './main.server';
|
||||||
|
import { resolveRequestOrigin } from './core/request-origin';
|
||||||
|
|
||||||
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
|
||||||
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
const browserDistFolder = resolve(serverDistFolder, '../browser');
|
||||||
@@ -39,13 +40,14 @@ app.get(
|
|||||||
* Handle all other requests by rendering the Angular application.
|
* Handle all other requests by rendering the Angular application.
|
||||||
*/
|
*/
|
||||||
app.get('**', (req, res, next) => {
|
app.get('**', (req, res, next) => {
|
||||||
const { protocol, originalUrl, baseUrl, headers } = req;
|
const { originalUrl, baseUrl } = req;
|
||||||
|
const origin = resolveRequestOrigin(req);
|
||||||
|
|
||||||
commonEngine
|
commonEngine
|
||||||
.render({
|
.render({
|
||||||
bootstrap,
|
bootstrap,
|
||||||
documentFilePath: indexHtml,
|
documentFilePath: indexHtml,
|
||||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
url: `${origin ?? 'http://localhost:4000'}${originalUrl}`,
|
||||||
publicPath: browserDistFolder,
|
publicPath: browserDistFolder,
|
||||||
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user