Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -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',
Expand Down
18 changes: 17 additions & 1 deletion apps/docs/lib/seo.ts
Original file line number Diff line number Diff line change
@@ -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';

/**
Expand Down
28 changes: 25 additions & 3 deletions apps/docs/middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down Expand Up @@ -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);
}
Expand Down