You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
docs site: no metadataBase and no canonical link on any page #12234
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.
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.
Same for the homepage, the blog index and blog posts.
Acceptance
every page type emits exactly one <link rel="canonical">, absolute, on https://objectstack.ai
/docs/x?utm_source=y and /docs/x both 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:
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 main by then. #12235 is queued behind this card because it edits the same generateMetadata block.
⚠️ Trap carried in from #12232's dev report — read before starting this card.
apps/docs/app/page.tsx is a shadowed route: proxy.ts rewrites / to /en, so RootPage() never runs (measured: curl / returns 200 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 adds metadataBase or a canonical link to app/page.tsx will 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 from scripts/pm/dispatch-gates.mjs --tier
Clause-②: no
Serial constraints cleared: apps/docs/lib/site.ts landed on main in 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.
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, base af58a6fbc4e51f3a1473c0c8a36cd6cbae3f5d0c
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
Premise 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:183
Positive control returns 4 hits, so the zero is absence, not a broken query. Premise holds.
apps/docs/lib/site.ts is on main and 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.
{
"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.ts rather 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 resolves content/docs/index.mdx. All five now emit exactly one absolute canonical, verified against a next build && next start server rather than against the edited files.
Query-string case measured: /docs/data-modeling/objects?utm_source=y&gclid=z canonicalises 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 (remove metadataBase, add a relative openGraph.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 than metadataBase-relative ones, deliberately: the helper throws at build time on a non-site-relative path, and it is the same helper app/sitemap.ts uses, 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.ai while the sitemap emits https://objectstack.ai/. RFC 3986 §6.2.3 makes an empty path equivalent to / and search engines normalise them identically. Harmonising would mean editing app/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: metadataBase is now set, so relative image URLs resolve. That was the dependency this card existed to satisfy.
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.