Skip to content

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

Closed

Description

@hotlong

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.tsx half 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.app must still 308 to docs.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.ts runs in the edge runtime, where importing lib/seo would drag the fumadocs source loader in, so collapsing those is a design question rather than the one-line move robots.ts turned out to be.

Activity

  1. added theissue type on Aug 26, 2026
  2. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Aug 26, 2026
  3. self-assigned this
    on Aug 26, 2026
  4. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    Claim: PM loop round 9
    Session: session_01G5zjYc2BoFV2NjKBBapC7C
    Branch: claude/issue-199-collapse-hostname-literals
    Worktree: objectos-issue-199
    Domain: 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 on llms-full.txt/route.ts and lib/source.ts. Neither touches your three files.

    One read coupling to respect: lib/seo.ts is imported by lib/source.ts, which #197 may edit. Changing what seo.ts exports 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.tsx was restructured by #168 (8feb90d) — it is a pass-through now, with the document shell in app/[lang]/layout.tsx. middleware.ts is unchanged today but four cards this round have touched routing around it. Read the tree.


    Generated by Claude Code

  5. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    Dev claim (os-dev)

    Session: session_01G5zjYc2BoFV2NjKBBapC7C
    Branch: claude/issue-199-collapse-hostname-literals (pushed, empty, off origin/main @ 1ebd7b7)
    Worktree: /home/user/objectos-issue-199
    File surface: apps/docs/app/layout.tsx, apps/docs/middleware.ts, apps/docs/lib/seo.ts. Stop on breach.
    Read coupling respected: lib/seo.ts exports will only be added to, never altered or removed (lib/source.ts is in flight on #197).

    Generated by Claude Code


    Generated by Claude Code

  6. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    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

  7. hotlong commented on Aug 26, 2026

    @hotlong
    ContributorAuthor

    ACCEPT — merging #204.

    Surface is exactly the three files granted, build and Node floor both green on 3f9394b, 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:

    baseline middleware imports @/lib/seo
    edge runtime JS 149,491 B 14,849,913 B (~99x)
    next build exit 0 0

    The 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.ts used 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 .next would 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/architecture spot 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_URL and CANONICAL_HOST agree. The dev's proposal — a leaf lib/site.ts holding 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/docs are each the only copy of a distinct host, which the report established file-by-file rather than by counting matches.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Labels

pm:dispatchedDispatched to a dev agent by /pm-dispatch

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions