Repository navigation
docs site: no metadataBase and no canonical link on any page #12234
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentationpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVP
on Aug 25, 2026 Held for round 2 — serial, not blocked. This card imports the shared origin constant that #12232 (dispatched now) owns creating; dispatching both at once means two agents creating the same file. Known trap to carry into the dispatch: read what #12232 actually exported before writing the import, and do not re-create the constant if it is already on
mainby then. #12235 is queued behind this card because it edits the samegenerateMetadatablock.⚠️ Trap carried in from #12232's dev report — read before starting this card.apps/docs/app/page.tsxis a shadowed route:proxy.tsrewrites/to/en, soRootPage()never runs (measured:curl /returns200 text/html, not the 307 that file would produce). Filed standalone as #12255.What it means here: the homepage's real metadata lives in
apps/docs/app/[lang]/page.tsx. An agent that addsmetadataBaseor a canonical link toapp/page.tsxwill ship a green PR that changes nothing served. Verify against a rendered response, not against the file you edited.The epic does not depend on #12255 being fixed — it stays a standalone finding — but this card does depend on knowing about it.
Claim: PM loop round 3 (epic PM for #12243)
Session:session_f9f0958b-ab68-46cc-801c-216aa7ee2107
Branch:claude/issue-12234-metadatabase-canonical
Worktree:objectstack-issue-12234
Domain:domain:devx
File surface:apps/docs/app/layout.tsx,apps/docs/app/[lang]/page.tsx,apps/docs/app/[lang]/docs/[[...slug]]/page.tsx,apps/docs/app/[lang]/blog/[[...slug]]/page.tsx(stop on breach; explain in the report)
Container & model: M,mode:subagent,model: opus— no path-derived mandate fromscripts/pm/dispatch-gates.mjs --tier
Clause-②: no
Serial constraints cleared:apps/docs/lib/site.tslanded onmainin PR #12253 (merged 16:31:53Z) — import it, do not re-create it. #12240 (JSON-LD) and #12242 (hero poster) are queued BEHIND this card: both touch files in this surface.Claim (os-dev, round 3): implementing this card.
Session:
session_f9f0958b-ab68-46cc-801c-216aa7ee2107(os-dev subagent dispatched by the epic PM for #12243)
Branch:claude/issue-12234-metadatabase-canonical(pushed empty at start as a write probe — green)
Worktree:objectstack-issue-12234, baseaf58a6fbc4e51f3a1473c0c8a36cd6cbae3f5d0c
File surface:apps/docs/app/layout.tsx,apps/docs/app/[lang]/page.tsx,apps/docs/app/[lang]/docs/[[...slug]]/page.tsx,apps/docs/app/[lang]/blog/[[...slug]]/page.tsxPremise re-measured on the branch base (post round-2 merges):
$ grep -rn "metadataBase\|canonical\|alternates" apps/docs --include='*.ts' --include='*.tsx' apps/docs/lib/site.ts:6,11,12,26 <- prose in the docblock only, no usage $ grep -rn "generateMetadata\|export const metadata" apps/docs --include='*.ts' --include='*.tsx' apps/docs/app/layout.tsx:5 apps/docs/app/[lang]/page.tsx:24 apps/docs/app/[lang]/docs/[[...slug]]/page.tsx:56 apps/docs/app/[lang]/blog/[[...slug]]/page.tsx:183Positive control returns 4 hits, so the zero is absence, not a broken query. Premise holds.
apps/docs/lib/site.tsis onmainand will be imported, not re-created.apps/docs/app/page.tsx(shadowed, #12255) will not be touched; every claim gets verified against a rendered response.os-dev-report
{ "issue": 12234, "status": "done", "branch": "claude/issue-12234-metadatabase-canonical", "pr": "https://github.com/objectstack-ai/objectstack/pull/12305", "premise_still_valid": true, "summary": "Re-measured on the branch base af58a6fbc (post round-2 merges): zero metadataBase/alternates/canonical usage under apps/docs (only prose in lib/site.ts's docblock), positive control 4 hits, and 0 rendered canonicals on every route type. Premise holds. apps/docs/app/layout.tsx now sets metadataBase: new URL(SITE_ORIGIN) importing the existing apps/docs/lib/site.ts (not re-created, no env var, no second origin constant), and the three page files that own metadata each add alternates.canonical built with absoluteUrl(). Absolute rather than metadataBase-relative on purpose: absoluteUrl() throws at build time on a non-site-relative path, so the canonical cannot silently land on another host, and it is the same helper app/sitemap.ts uses, so the two surfaces read one constant. app/page.tsx untouched (shadowed route, #12255) - every claim measured against a rendered response from `next build && next start`, never against an edited file. CORRECTION TO PM ASSUMPTION 3: the route tree has FIVE URL shapes across three page files, not four. The card's list (homepage, docs pages, blog index, blog posts) omits /docs, the empty-slug case of the docs optional catch-all, which resolves content/docs/index.mdx. All five are covered and verified. NOTE, NOT A DEFECT: the homepage canonical renders as https://objectstack.ai while the sitemap emits https://objectstack.ai/ - absoluteUrl('/') returns the trailing slash and Next strips it per trailingSlash:false. RFC 3986 6.2.3 makes an empty path equivalent to /, and search engines normalise them identically, so it is not a duplicate-content signal; every other URL matches byte for byte. Harmonising would mean editing app/sitemap.ts, outside the declared file surface, so it is flagged rather than reached for. No changeset (apps/docs is private:true and publishes nothing); skip-changeset added via the additive endpoint and read back after the size-labeler settled: [documentation, size/s, skip-changeset].", "tests": "All at final commit 8480b7598 (git rev-parse --short HEAD from the same run). BEFORE, base af58a6fbc, dev server: canonical_count=0 on /, /docs, /docs/data-modeling/objects, /blog, /blog/protocol-first-development, /docs/x?utm_source=y. AFTER, PRODUCTION server (next build && next start -p 3962), exactly one absolute canonical each: / -> https://objectstack.ai | /docs -> https://objectstack.ai/docs | /docs/data-modeling/objects -> https://objectstack.ai/docs/data-modeling/objects | /blog -> https://objectstack.ai/blog | /blog/protocol-first-development -> https://objectstack.ai/blog/protocol-first-development | /docs/getting-started -> https://objectstack.ai/docs/getting-started | /docs/ai/agents -> https://objectstack.ai/docs/ai/agents | /blog/metadata-driven-architecture -> https://objectstack.ai/blog/metadata-driven-architecture. QUERY-STRING CASE: /docs/data-modeling/objects?utm_source=y&gclid=z -> 200, canonical https://objectstack.ai/docs/data-modeling/objects (identical to the clean URL). NO REGRESSIONS, same prod server: /en 307->/ , /en/docs 307->/docs, /foo.txt 404, /llms.txt 200, /robots.txt 200, /sitemap.xml 200, /docs/getting-started.mdx 200. GATES, each quoted from its own verdict line, exit codes captured before any pipe: `pnpm --filter @objectstack/docs typecheck` exit 0 '✓ Types generated successfully'; `next build` exit 0 '✓ Compiled successfully in 4.9s', 1222 static pages, zero warn lines; check:docs-locale-catch-all '✓ 1 top-level dynamic segment(s), 1 guarded'; check:page-declaration-shape 'OK - 34 page entries across 2184 sources'; check:published-files '✓ 69 publishable package(s) of 78'; check:test-source-alias 'OK - 72 packages with tests scanned'; check:type-source-resolution 'OK - 93 tsc program(s) across 77 packages'; check:nul-bytes 'OK (scanned 6811 text file(s) ... no raw ASCII control bytes)'. Family derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` against the real 4-path changeset; it named the first five, nul-bytes is owed by any edit. ABLATION 1 (base build, metadataBase warning): four files reverted in place to af58a6fbc, mutation proven on disk by git hash-object 16f5cc60 != 0bc2cb10 and grep -c metadataBase = 0, then a full rebuilt `next build` -> exit 0, ZERO metadataBase mentions. Restore leg: git checkout HEAD -- apps/docs/app under an EXIT/INT/TERM trap with absolute paths, proven by git diff HEAD empty AND blob hash 0bc2cb10 == HEAD blob. So the base build never warned - Next only warns when metadata holds a RELATIVE url needing resolution, and the base tree had none. Acceptance bullet 3 is satisfied vacuously; reported rather than ticked. ABLATION 2 (is the warning live at all): metadataBase removed AND a relative openGraph.images added, both legs hash-confirmed and marker-counted on disk before building (removed_text_remaining=0, injected_marker_count=1), rebuilt -> '⚠ metadataBase property in metadata export is not set for resolving social open graph or twitter images, using \"http://localhost:3000\"'. Restored, git diff HEAD empty, both blob hashes matched HEAD. Predicted direction (warning appears) matched the observation. Repo-wide `pnpm lint` NOT run - declared narrowing, stated in the PR body; CI owns that run. Verification also ran UNLOCKED: scripts/pm/os-verify-lock.sh reports no usable flock on this macOS host and printed 'VERDICT command-exit 0 - UNLOCKED (declared)'; its official wording is pasted in the PR body.", "open_questions": [], "out_of_scope_findings": [] }ACCEPT — PR #12305, 4 files, +32/-0, pure additions on exactly the declared surface, importing the existing
lib/site.tsrather than re-creating it.- Correction to my assumption 3, and it is the useful kind: the route tree has five URL shapes, not the four I listed. I omitted
/docs— the empty-slug case of the docs optional catch-all, which resolvescontent/docs/index.mdx. All five now emit exactly one absolute canonical, verified against anext build && next startserver rather than against the edited files. - Query-string case measured:
/docs/data-modeling/objects?utm_source=y&gclid=zcanonicalises to the clean URL, byte-identical to what the clean URL emits. - An acceptance box reported as vacuous instead of ticked: "the build stops warning about a missing
metadataBase" — the base build never warned, because Next only warns when metadata holds a relative URL needing resolution, and there were none. Rather than claim a fix for a warning that never fired, the dev ran a second ablation (removemetadataBase, add a relativeopenGraph.images) and made the warning appear, proving it is live and that the box is satisfied vacuously rather than untested. That distinction is the whole difference between a verified gate and a green light nobody checked. - Absolute canonicals via
absoluteUrl()rather thanmetadataBase-relative ones, deliberately: the helper throws at build time on a non-site-relative path, and it is the same helperapp/sitemap.tsuses, so the canonical link and the sitemap entry read one constant and cannot drift. - Flagged, not a defect, and I agree with the reading: the homepage canonical renders
https://objectstack.aiwhile the sitemap emitshttps://objectstack.ai/. RFC 3986 §6.2.3 makes an empty path equivalent to/and search engines normalise them identically. Harmonising would mean editingapp/sitemap.ts, outside the declared surface — correctly flagged instead of reached for. No follow-up card: a card for a non-defect is noise.
⚠️ For #12235 (Open Graph), which is queued behind this:metadataBaseis now set, so relative image URLs resolve. That was the dependency this card existed to satisfy.- Correction to my assumption 3, and it is the useful kind: the route tree has five URL shapes, not the four I listed. I omitted
One-liner
Not one page on the site emits
<link rel="canonical">, and the root layout sets nometadataBase. Every URL variant of a page — query strings, tracking parameters, the/en/prefixed form — is a separate document to a search engine, with nothing declaring which one is real.Measured
Without
metadataBase, any relative URL handed toopenGraph/alternatesresolves against a build-time guess rather than the real origin — which is why it must land before, or with, the Open Graph card.Expected
apps/docs/app/layout.tsx:metadataBase: new URL('https://objectstack.ai'), from the same shared origin constant the sitemap uses.apps/docs/app/[lang]/docs/[[...slug]]/page.tsxgenerateMetadata:alternates: { canonical: page.url }.Acceptance
<link rel="canonical">, absolute, onhttps://objectstack.ai/docs/x?utm_source=yand/docs/xboth canonicalise to/docs/xmetadataBaseSource
Found in an SEO review of the docs site (
apps/docs) run on 2026-08-25, measured against the local dev server and against production. The canonical origin ishttps://objectstack.ai— maintainer ruling recorded in #10659: