Repository navigation
Description
Raised by the #178 dev after that card collapsed robots.ts's own BASE into SITE_URL, taking hostname literals in apps/docs from four to three. Two are left, and they are not the same job.
What is left
1. apps/docs/app/layout.tsx — metadataBase. Declares new URL('https://docs.objectos.ai') as the base every relative metadata URL resolves against. Very likely the same one-line import that robots.ts took, but nobody has tried it — confirm rather than assume, since layout.tsx sits above the locale segment and was restructured by #168 (8feb90d).
2. apps/docs/middleware.ts — the canonical-domain redirect. This one is a design question, not a move. Middleware runs in the edge runtime, and importing lib/seo.ts there would drag the fumadocs source loader in with it. So it needs either a constant that lives somewhere both the edge runtime and the loader can reach, or a deliberate decision that middleware keeps its own copy.
That second option is defensible: middleware's literal answers "which host is canonical" for a redirect that runs before any page code, and coupling it to the module graph that builds pages has a cost of its own. But it should be a decision on the record, not a leftover.
Why it is worth a card rather than a note
lib/seo.ts:4 already carries the comment "Canonical production host. Keep in sync with middleware's domain redirect." — a hand-maintained sync obligation, written down and unenforced. That is the same shape as #173's STATIC_PAGE_LOCALES: correct today, silently wrong the day someone changes one side. The docs site has a canonical host in three places and a comment asking a human to remember.
Cheap and low-stakes — nothing is broken today, and both literals hold the same value. It is worth doing while the direction is fresh, and worth doing as one card so the middleware question gets answered rather than deferred a second time.
Constraints
- Do the
layout.tsxhalf only if it really is the one-line import. If it turns out to need more, stop and report — that would make it a second design question and this card would need re-scoping. - For
middleware.ts, either outcome is acceptable: a shared constant that both runtimes can import, or a documented decision that it keeps its own literal. What is not acceptable is leaving the existing comment as the only mechanism. If the decision is "keep the copy", say so in code where both sides can see it, and make the comment state the reason rather than issuing an instruction to a human. - Verify the middleware redirect still works after any change —
www.objectos.appmust still 308 todocs.objectos.ai. It is the kind of thing that fails silently in production and nowhere else.
Provenance
Reported by the #178 dev rather than filed, with the edge-runtime constraint already diagnosed:
middleware.tsruns in the edge runtime, where importinglib/seowould drag the fumadocs source loader in, so collapsing those is a design question rather than the one-line moverobots.tsturned out to be.
Activity
hotlong commentedon Aug 26, 2026
Claim: PM loop round 9
Session:
session_01G5zjYc2BoFV2NjKBBapC7CBranch:
claude/issue-199-collapse-hostname-literalsWorktree:
objectos-issue-199Domain: n/a (objectos carries no
domain:*lanes)File surface:
apps/docs/app/layout.tsx,apps/docs/middleware.ts,apps/docs/lib/seo.ts. Stop on breach; explain in the report.Container & model: M,
mode:subagent,model: opus— PM judgement.Clause-②: no
Serial constraints cleared: #184 is in flight on
.github/scripts/check-locale-surface.mjs, #197 onllms-full.txt/route.tsandlib/source.ts. Neither touches your three files.One read coupling to respect:
lib/seo.tsis imported bylib/source.ts, which #197 may edit. Changing whatseo.tsexports would reach them; changing what it holds internally would not. If your fix needs to alter or remove an existing export rather than add one, stop and report.Note
app/layout.tsxwas restructured by #168 (8feb90d) — it is a pass-through now, with the document shell inapp/[lang]/layout.tsx.middleware.tsis unchanged today but four cards this round have touched routing around it. Read the tree.Generated by Claude Code
hotlong commentedon Aug 26, 2026
Dev claim (os-dev)
Session:
session_01G5zjYc2BoFV2NjKBBapC7CBranch:
claude/issue-199-collapse-hostname-literals(pushed, empty, offorigin/main@1ebd7b7)Worktree:
/home/user/objectos-issue-199File surface:
apps/docs/app/layout.tsx,apps/docs/middleware.ts,apps/docs/lib/seo.ts. Stop on breach.Read coupling respected:
lib/seo.tsexports will only be added to, never altered or removed (lib/source.tsis in flight on #197).Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 26, 2026
os-dev-report
{ "issue": 199, "status": "done", "branch": "claude/issue-199-collapse-hostname-literals", "pr": "https://github.com/objectstack-ai/objectos/pull/204", "head_sha": "3f9394b", "premise_still_valid": true, "files_changed": [ "apps/docs/app/layout.tsx", "apps/docs/lib/seo.ts", "apps/docs/middleware.ts" ], "summary": "Half 1 was the one-line import the card predicted: app/layout.tsx now derives metadataBase from SITE_URL, confirmed rather than assumed (layout.tsx is a pass-through since #168 but still owns locale-independent metadata, and seo -> source is acyclic). Half 2 landed as the card's second acceptable outcome, a documented decision, after measuring the edge-runtime claim instead of inheriting it: pointing middleware at @/lib/seo takes the edge bundle from 149,491 B to 14,849,913 B (~99x, 389 MDX frontmatter entries and toFumadocsSource inlined) with next build still exiting 0 -- so the cost lands at Cloudflare Workers deploy, not in CI. Middleware keeps its own literal via named CANONICAL_HOST/LEGACY_HOST constants; the stale 'Keep in sync with middleware's domain redirect' instruction on lib/seo.ts is gone, replaced by the reason on both sides plus an explicit statement that nothing enforces agreement. Canonical-host literals in apps/docs: 3 -> 2.", "gates": [ { "gate": "pnpm turbo run type-check --continue", "sha": "3f9394b", "result": "pass", "verdict_line": "Tasks: 1 successful, 1 total" }, { "gate": "pnpm turbo run build", "sha": "3f9394b", "result": "pass", "verdict_line": "Compiled successfully in 36.3s" }, { "gate": "pnpm turbo run test", "sha": "3f9394b", "result": "pass", "verdict_line": "Tasks: 1 successful, 1 total" }, { "gate": "node .github/scripts/check-node-floor.mjs", "sha": "3f9394b", "result": "pass", "verdict_line": "Every declared floor clears what the dependency tree requires, and the declarations agree." }, { "gate": "node .github/scripts/check-locale-surface.mjs", "sha": "3f9394b", "result": "pass", "verdict_line": "every advertised URL has a source file, and every source file is advertised (sitemap.xml 346/346, 0 unexpected, 0 missing)" }, { "gate": "CI workflow conclusion", "sha": "3f9394b", "result": "in_progress", "verdict_line": "not awaited -- PM owns CI convergence" } ], "tests": "REDIRECT, against a running build (next start -p 31990, curl with Host header, own PID recorded and killed by PID): www.objectos.app/docs/architecture -> 308 https://docs.objectos.ai/docs/architecture; / -> 308 https://docs.objectos.ai/; ?q=1 preserved; /zh-Hans/docs/architecture -> 308 to the same path on the canonical host. Controls: Host docs.objectos.ai returns 200 (not redirected away), and /cn/... -> /zh-Hans/... still 308. METADATA UNCHANGED: a raw byte diff of .next is useless (Next salts every HTML/RSC payload with a fresh build ID, so all 6152 prerendered outputs differ between any two builds -- observed, not assumed). Instead every URL-bearing head tag (canonical, og:*, twitter:*, hreflang alternates, icon, title) was extracted from all 577 prerendered pages pre-change and post-change: 12347 rows, diff exit 0, 0 differing lines. robots.txt/sitemap.xml/llms.txt bodies sha256-identical. Edge runtime JS 149,491 B before and after. PROBE (the edge-runtime measurement) was a mutation run with a trap and absolute paths: mutation proven on disk by occurrence counts (inserted import 1, removed literal 0) AND blob hash change (58366ec -> 4bdbda8); restore proven by git diff HEAD empty plus on-disk blob == HEAD blob 58366ec, not by an exit code. The reverse leg (rebuilding the pre-change tree for the metadata comparison) was run from the committed state and each of the three files was verified back to its HEAD~1 blob before the build, then forward to HEAD after.", "open_questions": [ { "question": "Take the leaf-module option and collapse the last two canonical-host literals to one, or keep the documented duplication this PR lands?", "options": [ "A -- keep as landed: two literals, reason recorded on both sides, nothing enforcing agreement. Zero new files, stays inside the granted surface.", "B -- add lib/site.ts, a leaf module holding only the host, imported by both lib/seo.ts and middleware.ts. Collapses 2 -> 1 with no edge cost (leaf module pulls in no loader) and lib/seo.ts keeps SITE_URL as a re-export, so its public surface is unchanged and the #197 read coupling stays untouched." ], "recommendation": "B is the better end state and is cheap, but it needs a fourth file, which is outside the three-file surface this card granted -- so it was not done. Flagged rather than taken: the card authorized either outcome and this is the maintainer's call on review. A is fully defensible on its own and is what is on the branch." } ], "out_of_scope_findings": [], "deviations": [ "None on file surface: exactly the three granted files. lib/seo.ts gained no export and lost none (comment above SITE_URL only), so the lib/source.ts read coupling with #197 is untouched.", "No changeset: this repo has no changeset flow, and no skip-changeset label mechanism exists here (that label is objectstack-only and was deliberately not created).", "Local gate scope was narrowed to the five gates CI actually runs (ci.yml: type-check, build, test, check-node-floor, check-locale-surface) plus the two workflows untouched by this diff (translations.yml reads content/, deploy-docs.yml runs on merge). Declared, not skipped.", "GitHub appended a second 'Generated by Claude Code' footer to the PR body on creation. Platform behaviour, left alone rather than edited.", "Cosmetic: middleware.ts hoists two existing literals to named constants rather than leaving them inline. That is what gives the recorded decision a place to attach and makes the two sides symmetric (SITE_URL / CANONICAL_HOST); it changes no behaviour, as the live redirect table shows." ] }Generated by Claude Code
Generated by Claude Code
hotlong commentedon Aug 26, 2026
ACCEPT — merging #204.
Surface is exactly the three files granted,
buildandNode floorboth green on3f9394b, and the locale-surface gate reports 346/346 with 0 unexpected and 0 missing, so the metadata artifacts are provably untouched by this refactor.The part worth recording is the method, not the diff.
I asked this card to verify the #178 dev's edge-runtime claim rather than trust it. That claim was reasoned from the module graph — entirely plausible, and the kind of thing that normally gets adopted as fact by the next agent to read it. This report built it both ways instead:
@/lib/seonext buildexitThe claim holds. But the decisive row is the second one: the build succeeds. A 14.7 MB edge bundle produces no error, no warning, and no failing check. This app deploys to Cloudflare Workers via
opennextjs-cloudflare, so the bill arrives at deploy time, on a change that looked like a one-line import cleanup and passed every gate we have. That is a class of failure our CI cannot see, and it is now written down in the two files where someone would actually be standing when they were about to cause it.Decision accepted: middleware keeps its own literal. What changed is that neither side issues an instruction any more.
lib/seo.tsused to say "Keep in sync with middleware's domain redirect" — an unenforced order to a future human, which is the exact shape the card objected to. Both sides now state the reason instead. The canonical-host question is answered before any page code runs, and that answer should not depend on the module graph that builds pages.Verification also chose the right instrument. A raw byte diff of
.nextwould have been noise — Next salts every build with a fresh build ID, so all 6,152 prerendered outputs differ between any two builds and the diff proves nothing. Extracting the 12,347 URL-bearing head-tag rows across 577 prerendered pages and getting 0 differing lines is an actual measurement of the property in question. The/zh-Hans/docs/architecturespot check confirms the #166/#174 fallback behaviour survived: it still canonicalizes to the English URL.Redirect exercised against a running server across six cases, including query preservation and the legacy
/cn/locale control — not read off the source.Residual, kept open rather than closed by assertion: nothing enforces that
SITE_URLandCANONICAL_HOSTagree. The dev's proposal — a leaflib/site.tsholding just the host, imported by both — is right, and was correctly left undone because it needs a fourth file this card did not grant. Filing it as a follow-up rather than letting it dissolve into the merge.Hostname literals for the canonical docs host: 3 → 2. The remaining three literals in
apps/docsare each the only copy of a distinct host, which the report established file-by-file rather than by counting matches.Generated by Claude Code