Skip to content

refactor(docs): give the canonical host one definition in a leaf module - #227

Merged
hotlong merged 1 commit into
mainfrom
claude/issue-205-site-host-leaf-module
Aug 26, 2026
Merged

refactor(docs): give the canonical host one definition in a leaf module#227
hotlong merged 1 commit into
mainfrom
claude/issue-205-site-host-leaf-module

Conversation

@hotlong

@hotlong hotlong commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #205

SITE_URL in lib/seo.ts and CANONICAL_HOST in middleware.ts were two literals for one value. apps/docs/lib/site.ts now holds the host and nothing else; lib/seo.ts derives the origin from it, middleware.ts redirects to it. app/layout.tsx needed no change — it takes SITE_URL from lib/seo.ts, which is still exported from there, just no longer written out there.

All measurements below are at 98ecc0e, the branch head.

The size, which is the acceptance criterion

Edge runtime JS = the sum of the JS chunks the middleware entry names in .next/server/middleware-manifest.json. The route table next build prints reports no size for middleware, so there is nothing to read there instead.

tree edge runtime JS next build
baseline, 0c4351f 149,745 B exit 0
baseline rebuilt, no source change (control) 149,745 B exit 0
this branch, 98ecc0e 149,745 B exit 0
ablation: middleware.ts importing @/lib/seo 17,375,914 B exit 0

Zero growth — and stronger than equal totals: the three chunk files are byte-for-byte identical to the baseline, same content-hashed names, same sizes ([root-of-the-server]__07_86ae._.js at 139,218 B in both). The import adds no module-wrapper overhead at all; the bundler inlines the constant exactly as it inlined the local one.

The baseline was re-measured rather than assumed. #205 recorded 149,491 B; the current figure is 149,745 B, 254 B higher, consistent with #223 and #225 having landed since.

The control row is the noise floor. Two builds of unmodified source produced the identical byte count, so the 0 B delta on this branch is a measurement and not a coincidence.

The ablation row was measured on this tree, not quoted. #205 carried 14,849,913 B from #204's run; on today's content, with zh-Hant shipped, pointing middleware.ts at @/lib/seo produces 17,375,914 B — 116x, not 99x, and growing with the collection. next build exited 0, as advertised. The mutation was confirmed on disk before the build (grep counts on both the injected and the removed text) and the restore was verified by git hash-object against the HEAD blob, with git diff HEAD empty afterwards.

apps/docs/lib/site.ts imports nothing. It is one exported string and a comment explaining why it must stay that way.

Rendered metadata unchanged

#204's method: every URL-bearing link/meta tag in the head of all 659 prerendered HTML files, plus every absolute URL in robots.txt, sitemap.xml, llms.txt and llms-full.txt — 12,696 rows, sorted, diffed.

baseline vs branch:   diff exit 0, 0 lines
control vs baseline:  diff exit 0, 0 lines     (proves the extraction is build-salt-insensitive)

No byte diff of .next was attempted; Next salts every build, so that comparison says nothing.

The domain redirect

Six cases against next start on the branch build, plus four controls for what #223 and #225 established. Server killed by explicit PID after confirming its cwd was inside this worktree.

case request Host path result
1 www.objectos.app /docs/quickstart 308 https://docs.objectos.ai/docs/quickstart
2 www.objectos.app /docs/quickstart?utm_source=x&q=a%20b 308 https://docs.objectos.ai/docs/quickstart?utm_source=x&q=a+b
3 www.objectos.app / 308 https://docs.objectos.ai/
4 docs.objectos.ai /docs/quickstart 200
5 local /cn/docs/quickstart 308 /zh-Hans/docs/quickstart
6 www.objectos.app /cn/docs/quickstart 308 https://docs.objectos.ai/cn/docs/quickstart
7 local /zh-Hant/docs/quickstart 200
8 local /zh-hant/docs/quickstart 308 /zh-Hant/docs/quickstart
9 local /en/docs/quickstart 307 /docs/quickstart

Case 2 preserves the query. Case 6 is the ordering control: the domain rule runs before the legacy-locale rule, so the host is corrected first and /cn/ is handled on the next hop. Cases 7 to 9 are #225's folded-index behaviour, untouched.

One definition is now an enforced definition

The card was filed because nothing failed if the two values drifted. They can no longer drift — there is one of them — and moving that one is caught by a gate that already exists.

.github/scripts/check-locale-surface.mjs keeps its own copy of the host on purpose, documented in place as the independent oracle: "a check that shares a constant with the thing it checks cannot catch that constant being wrong." It compares the built sitemap.xml against URLs it constructs from its own literal. sitemap.ts builds its URLs through lib/seo.ts, which now derives from SITE_HOST — so the gate transitively pins the middleware's redirect host as well, which it could not do before.

Measured, not asserted. Setting SITE_HOST to docs.objectos.invalid and rebuilding:

check-locale-surface.mjs   exit 1
✗ locale surface: 818 finding(s)

Restored and verified by hash against the HEAD blob. As a side observation, that run put the edge bundle at 149,750 B — exactly 5 B more, the length difference between the two hostnames. The host string is inlined into the edge chunk and the size tracks it byte for byte, which is independent confirmation that nothing but the string crosses the import.

Gates

All at 98ecc0e, all with --force because the turbo cache is shared across worktrees in this container, and all with the exit code captured before any pipe.

command exit
turbo run build --force --filter=@objectos/docs 0, 1140/1140 pages
turbo run type-check --force --filter=@objectos/docs 0
turbo run test --force 0
node .github/scripts/check-locale-surface.mjs 0, sitemap.xml 409 URLs, 0 unexpected, 0 missing

No changeset: this repo has no .changeset directory and no changeset workflow.

Out of scope, filed


Generated by Claude Code

`SITE_URL` in `lib/seo.ts` and `CANONICAL_HOST` in `middleware.ts` were two
literals for one value with nothing enforcing they agreed. `lib/site.ts` now
holds the host and nothing else; `lib/seo.ts` derives the origin from it and
`middleware.ts` redirects to it.

The leaf constraint is the point: `middleware.ts` runs in the edge runtime and
`lib/seo.ts` reaches `lib/source.ts`, so importing the host from there pulls the
whole MDX collection into the edge bundle while `next build` still exits 0.
`lib/site.ts` imports nothing, and each of the three files says why.

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 10:12
@hotlong
hotlong merged commit f78a077 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.

SITE_URL and middleware's CANONICAL_HOST are two literals for one value, with nothing enforcing agreement

2 participants