diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b259b9817..cc58114fc 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -43,9 +43,14 @@ export default function ({ children }) { ); } -export const metadata: Metadata = { - title: { - template: '%s | Umami', - default: 'Umami', - }, -}; +export async function generateMetadata(): Promise { + const headerStore = await headers(); + + return { + metadataBase: getBaseUrl(headerStore), + title: { + template: '%s | Umami', + default: 'Umami', + }, + }; +} diff --git a/src/lib/__tests__/get-base-url.test.ts b/src/lib/__tests__/get-base-url.test.ts new file mode 100644 index 000000000..5bca0f193 --- /dev/null +++ b/src/lib/__tests__/get-base-url.test.ts @@ -0,0 +1,48 @@ +import { HOMEPAGE_URL } from '../constants'; +import { getBaseUrl } from '../get-base-url'; + +function createHeaders(entries: Record) { + return { + get(name: string) { + return entries[name.toLowerCase()] ?? null; + }, + }; +} + +test('prefers forwarded host and protocol', () => { + const url = getBaseUrl( + createHeaders({ + 'x-forwarded-host': 'umami.is', + 'x-forwarded-proto': 'https', + host: 'localhost:3000', + }), + ); + + expect(url.toString()).toBe('https://umami.is/'); +}); + +test('falls back to host header', () => { + const url = getBaseUrl( + createHeaders({ + host: 'analytics.example.com', + }), + ); + + expect(url.toString()).toBe('https://analytics.example.com/'); +}); + +test('uses http for localhost hosts', () => { + const url = getBaseUrl( + createHeaders({ + host: 'localhost:3000', + }), + ); + + expect(url.toString()).toBe('http://localhost:3000/'); +}); + +test('falls back to homepage when host is missing', () => { + const url = getBaseUrl(createHeaders({})); + + expect(url.toString()).toBe(`${HOMEPAGE_URL}/`); +}); diff --git a/src/lib/get-base-url.ts b/src/lib/get-base-url.ts new file mode 100644 index 000000000..512b321a9 --- /dev/null +++ b/src/lib/get-base-url.ts @@ -0,0 +1,40 @@ +import { HOMEPAGE_URL } from './constants'; + +type HeaderStore = Pick; + +function getFirstHeaderValue(value?: string | null) { + return value?.split(',')[0]?.trim(); +} + +function getDefaultProtocol(host?: string) { + if (!host) { + return 'https'; + } + + if (host.startsWith('localhost') || host.startsWith('127.0.0.1') || host.startsWith('[::1]')) { + return 'http'; + } + + return 'https'; +} + +export function getBaseUrl(headers?: HeaderStore) { + const host = + getFirstHeaderValue(headers?.get('x-forwarded-host')) || + getFirstHeaderValue(headers?.get('host')); + + if (!host) { + return new URL(HOMEPAGE_URL); + } + + const protocol = + getFirstHeaderValue(headers?.get('x-forwarded-proto')) || + getFirstHeaderValue(headers?.get('x-forwarded-protocol')) || + getDefaultProtocol(host); + + try { + return new URL(`${protocol}://${host}`); + } catch { + return new URL(HOMEPAGE_URL); + } +}