diff --git a/apps/docs/app/layout.tsx b/apps/docs/app/layout.tsx index 3f8ddac..14cc545 100644 --- a/apps/docs/app/layout.tsx +++ b/apps/docs/app/layout.tsx @@ -1,9 +1,10 @@ import './global.css'; import type { ReactNode } from 'react'; import type { Metadata } from 'next'; +import { SITE_URL } from '@/lib/seo'; export const metadata: Metadata = { - metadataBase: new URL('https://docs.objectos.ai'), + metadataBase: new URL(SITE_URL), title: { template: '%s | ObjectOS', default: 'ObjectOS', diff --git a/apps/docs/lib/seo.ts b/apps/docs/lib/seo.ts index 766f526..45b3c94 100644 --- a/apps/docs/lib/seo.ts +++ b/apps/docs/lib/seo.ts @@ -1,7 +1,23 @@ import { i18n } from '@/lib/i18n'; import { source } from '@/lib/source'; -// Canonical production host. Keep in sync with middleware's domain redirect. +/** + * Canonical production host — the origin every absolute URL in this module is + * built on, and the `metadataBase` every relative metadata URL resolves against. + * + * `middleware.ts` spells the same host out a second time, and that duplication + * is a decision rather than an oversight. It cannot import this constant: this + * module imports `lib/source.ts`, so the import would drag the fumadocs loader + * and every compiled MDX module into the edge runtime the middleware runs in. + * Measured on this tree — middleware importing `SITE_URL` from here takes the + * edge bundle from 149 KB to 14.7 MB of JavaScript, with `next build` still + * exiting 0, so the cost surfaces at deploy rather than in CI. + * + * The reason is recorded on both sides instead of a sync instruction, because + * an instruction to a human is not a mechanism. Nothing here enforces that the + * two agree; collapsing them properly needs a leaf module both runtimes can + * import, which is a change to the file layout rather than to either file. + */ export const SITE_URL = 'https://docs.objectos.ai'; /** diff --git a/apps/docs/middleware.ts b/apps/docs/middleware.ts index 58366ec..8858b25 100644 --- a/apps/docs/middleware.ts +++ b/apps/docs/middleware.ts @@ -4,6 +4,28 @@ import { i18n } from '@/lib/i18n'; const LOCALE_COOKIE = 'FD_LOCALE'; +/** + * The canonical docs host, and the legacy host that redirects to it. + * + * `CANONICAL_HOST` holds the same value as `SITE_URL` in `lib/seo.ts`, written + * out a second time on purpose. This file runs in the **edge runtime**, and + * `lib/seo.ts` imports `lib/source.ts` — so importing the constant from there + * would pull the fumadocs loader and all 413 compiled MDX modules into this + * bundle. Measured on this tree: the edge bundle goes from 149 KB to 14.7 MB of + * JavaScript, and `next build` still exits 0, so nothing in CI would say so. + * + * Two copies of one hostname is the smaller cost, and the copy belongs on this + * side: this redirect answers "which host is canonical" for every request + * before any page code runs, and that answer should not depend on the module + * graph that builds pages. Collapsing the two would take a leaf module both + * runtimes can import — not an import from `lib/seo.ts`. + * + * Nothing enforces that the two agree. That is the residual cost of the + * decision, stated here so it is visible rather than discovered. + */ +const CANONICAL_HOST = 'docs.objectos.ai'; +const LEGACY_HOST = 'www.objectos.app'; + /** * Supported languages extracted from i18n configuration */ @@ -101,12 +123,12 @@ function getPreferredLanguage(request: NextRequest): string { * - Stores language preference as a cookie */ export default function middleware(request: NextRequest) { - // Canonical domain redirect: legacy docs host -> docs.objectos.ai (permanent). + // Canonical domain redirect: legacy docs host -> canonical host (permanent). const host = request.headers.get('host'); - if (host === 'www.objectos.app') { + if (host === LEGACY_HOST) { const target = new URL(request.url); target.protocol = 'https:'; - target.host = 'docs.objectos.ai'; + target.host = CANONICAL_HOST; target.port = ''; return NextResponse.redirect(target, 308); }