Skip to content

fix(docs): collapse the metadataBase hostname into SITE_URL, record the middleware decision - #204

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-199-collapse-hostname-literals
Aug 26, 2026
Merged

fix(docs): collapse the metadataBase hostname into SITE_URL, record the middleware decision#204
hotlong merged 1 commit into
mainfrom
claude/issue-199-collapse-hostname-literals

Conversation

@hotlong

@hotlong hotlong commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #199

Two halves, deliberately different jobs. The first is the one-line import the card expected. The second is answered with a measurement, and lands as a documented decision.

All readings below are from 3f9394b, the head of this branch.

1. app/layout.tsx — it really was the one-line import

metadataBase now derives from SITE_URL instead of repeating the host. Confirmed rather than assumed: layout.tsx is a pass-through since #168, but it still owns the locale-independent metadata, and lib/seo.ts is reachable from it with no cycle (seo imports source; source does not import seo — that is why SITE_NAME lives in source.ts).

Nothing else was needed, so the card does not need re-scoping.

2. middleware.ts — the edge-runtime claim, measured

The #178 dev's claim came from reading the module graph, not from a build. I built it both ways.

Probe: point middleware.ts at SITE_URL from @/lib/seo and build.

baseline middleware imports @/lib/seo
edge runtime JS 149,491 B 14,849,913 B (~99x)
toFumadocsSource in edge bundle 0 3
MDX frontmatter entries inlined 0 389
react-dom chunk in edge bundle absent 201,121 B
next build exit 0 0

The claim holds, and the last row is the part that matters: the build succeeds. Nothing in CI would report a 14.7 MB edge bundle. This app deploys to Cloudflare Workers (deploy-docs.yml runs opennextjs-cloudflare deploy), so the cost would land at deploy time — exactly the silent-in-production failure mode the card warns about.

The probe was reverted; its restore is proven by blob identity against HEAD, not by an exit code.

Decision: middleware keeps its own literal, as the card's second acceptable outcome. The 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.

What changed is that the decision is now in the code on both sides:

  • lib/seo.ts no longer says "Keep in sync with middleware's domain redirect." That instruction to a human was the unenforced-sync shape the card objected to. It now states the reason for the duplication, including the measurement and the fact that nothing enforces agreement.
  • middleware.ts hoists CANONICAL_HOST and LEGACY_HOST to named constants carrying the other half of the reason, so the copy is visible as a decision rather than as a stray literal mid-function.

Neither side issues an instruction. Both state why.

The residual, stated plainly

Nothing enforces that the two values agree. Collapsing them properly needs a leaf module both runtimes can import (a small lib/site.ts holding just the host, imported by lib/seo.ts and middleware.ts) — that is a new file, outside the three-file surface this card granted, so it is not done here. It is a real option and cheap; it is the maintainer's call, not mine.

Verification

The redirect, exercised against a running build (next start, not read off the source):

request result
Host: www.objectos.app /docs/architecture 308https://docs.objectos.ai/docs/architecture
Host: www.objectos.app / 308https://docs.objectos.ai/
Host: www.objectos.app /docs/architecture?q=1 308https://docs.objectos.ai/docs/architecture?q=1 (query kept)
Host: www.objectos.app /zh-Hans/docs/architecture 308https://docs.objectos.ai/zh-Hans/docs/architecture
Host: docs.objectos.ai /docs/architecture 200 (control: canonical host not redirected away)
Host: docs.objectos.ai /cn/docs/architecture 308/zh-Hans/docs/architecture (control: legacy locale redirect intact)

Rendered metadata is unchanged. A raw byte diff of .next is useless here — Next salts every HTML and RSC payload with a fresh build ID, so all 6,152 prerendered outputs differ between any two builds. Instead I extracted every URL-bearing head tag (canonical, og:*, twitter:*, hreflang alternates, icons, title) from all 577 prerendered pages and compared pre-change against post-change:

metadata rows compared : 12,347  (577 HTML files)
diff                   : 0 differing lines
robots.txt / sitemap.xml / llms.txt bodies : identical (sha256)
edge runtime JS        : 149,491 B before, 149,491 B after

Spot check on a fallback-locale page, which #166 and #174 made non-trivial — /zh-Hans/docs/architecture correctly points at the English canonical:

link rel="canonical" href="https://docs.objectos.ai/docs/architecture"
meta property="og:url" content="https://docs.objectos.ai/docs/architecture"
meta property="og:image" content="https://docs.objectos.ai/og/docs/en/architecture/image.png"

Gates, all at 3f9394b, each read from the command's own verdict line with the exit code captured before any pipe:

gate result
pnpm turbo run type-check --continue Tasks: 1 successful, 1 total
pnpm turbo run build Compiled successfully in 36.3s
pnpm turbo run test Tasks: 1 successful, 1 total
node .github/scripts/check-node-floor.mjs exit 0 — "Every declared floor clears what the dependency tree requires"
node .github/scripts/check-locale-surface.mjs exit 0 — "every advertised URL has a source file" (346/346, 0 unexpected, 0 missing)

Hostname literals in apps/docs after this change

Canonical docs host: 3 → 2.

# location note
1 lib/seo.ts:21 SITE_URL the source for all page + route metadata; app/layout.tsx now derives metadataBase from it
2 middleware.ts:26 CANONICAL_HOST the deliberate, documented copy

Three further hostname literals exist in apps/docs, none of them duplicates — each is the only copy of a distinct host:

location host why it is not a duplicate
middleware.ts:27 LEGACY_HOST www.objectos.app the source of the redirect, not the canonical host
lib/layout.shared.tsx:10 WEBSITE_URL www.objectos.ai the marketing site, a different property
next.config.mjs:27 objectstack.ai an image remotePatterns allowlist entry

Prose mentions in app/[lang]/privacy/page.tsx, app/[lang]/terms/page.tsx and a historical note in app/llms.txt/route.ts are reader-facing text, not configuration, and were left alone.

Scope

Three files, exactly the granted surface. lib/seo.ts gained no export and lost none — only the comment above SITE_URL changed — so the lib/source.ts read coupling with #197 is untouched. No changeset (this repo has no changeset flow).

Generated by Claude Code


Generated by Claude Code

…he middleware decision

`app/layout.tsx` now derives `metadataBase` from `SITE_URL` instead of
repeating the host, which takes the canonical-host literals in `apps/docs`
from three to two.

`middleware.ts` keeps its own copy, deliberately. Measured on this tree:
importing `SITE_URL` from `lib/seo.ts` pulls the fumadocs loader and every
compiled MDX module into the edge bundle, taking it from 149 KB to 14.7 MB
of JavaScript with `next build` still exiting 0. The reason is now recorded
on both sides, replacing the "keep in sync" instruction on `lib/seo.ts`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G5zjYc2BoFV2NjKBBapC7C
@hotlong
hotlong marked this pull request as ready for review August 26, 2026 03:05
@hotlong
hotlong merged commit 3433803 into main Aug 26, 2026
2 checks passed
hotlong added a commit that referenced this pull request Aug 26, 2026
…228)

The Host header carries host[:port], so comparing it whole against a bare hostname meant www.objectos.app:8080 was never equal to LEGACY_HOST. It fell through to locale negotiation and was served under the legacy host (200) instead of redirected off it (308). Three lines below, the same block already stripped the port from the target, so the port was accounted for on the way out and not on the way in.

Compares the hostname now. The target-side normalisation is unchanged.

Verified with the redirect table established by #204, #223, #225 and #227, plus two rows: the ported legacy host now 308s with no port on the target, and Host: docs.objectos.ai:3000 stays 200 on both sides -- which proves the comparison is against LEGACY_HOST specifically rather than a blanket port strip, a fix that would have passed every other row.

The before column was reproduced by rebuilding and running the pre-fix file, not inferred.

Closes #226
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Two hostname literals remain outside lib/seo.ts — one is a one-liner, one is an edge-runtime design question

2 participants