docs site: no metadataBase and no canonical link on any page #12234
Copy link
Copy link
Closed
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationdomain:devxpm:dispatchedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPrepo:objectstacksize/s
Description
One-liner
Not one page on the site emits <link rel="canonical">, and the root layout sets no metadataBase. 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
$ grep -rn "metadataBase\|canonical\|alternates" apps/docs --include=*.ts --include=*.tsx | grep -v node_modules
(no matches)
$ curl -s http://localhost:3999/docs/data-modeling/objects | grep -c 'rel="canonical"'
0
Without metadataBase, any relative URL handed to openGraph/alternates resolves 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 }.- Same for the homepage, the blog index and blog posts.
Acceptance
- every page type emits exactly one
<link rel="canonical">, absolute, onhttps://objectstack.ai -
/docs/x?utm_source=yand/docs/xboth canonicalise to/docs/x - the Next build no longer warns about a missing
metadataBase
Source
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 is https://objectstack.ai — maintainer ruling recorded in #10659:
这个仓的文档站规范 URL 是 https://objectstack.ai
Metadata
Metadata
Assignees
Labels
documentationImprovements or additions to documentationImprovements or additions to documentationdomain:devxpm:dispatchedpriority:p0Critical: blocker, must ship before MVPCritical: blocker, must ship before MVPrepo:objectstacksize/s
Activity
os-zhuang commentedon 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.os-zhuang commentedon Aug 25, 2026
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.
os-zhuang commentedon Aug 25, 2026
Claim: PM loop round 3 (epic PM for #12243)
Session:
session_f9f0958b-ab68-46cc-801c-216aa7ee2107Branch:
claude/issue-12234-metadatabase-canonicalWorktree:
objectstack-issue-12234Domain:
domain:devxFile 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 --tierClause-②: 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.os-zhuang commentedon Aug 25, 2026
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, baseaf58a6fbc4e51f3a1473c0c8a36cd6cbae3f5d0cFile 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):
Positive 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-zhuang commentedon Aug 25, 2026
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": [] }os-zhuang commentedon Aug 25, 2026
ACCEPT — PR #12305, 4 files, +32/-0, pure additions on exactly the declared surface, importing the existing
lib/site.tsrather than re-creating it./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./docs/data-modeling/objects?utm_source=y&gclid=zcanonicalises to the clean URL, byte-identical to what the clean URL emits.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.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.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.metadataBaseis now set, so relative image URLs resolve. That was the dependency this card existed to satisfy.