Repository navigation
docs site: no structured data (JSON-LD) anywhere #12240
Description
Activity
- addeddocumentationImprovements or additions to documentationImprovements or additions to documentationpriority:p2Medium: important, M3Medium: important, M3
on Aug 25, 2026 ⚠️ 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 adds JSON-LD 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 5 (epic PM for #12243)
Session:session_f9f0958b-ab68-46cc-801c-216aa7ee2107
Branch:claude/issue-12240-json-ld
Worktree:objectstack-issue-12240
Domain:domain:devx
File surface: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
Clause-②: no
Serial constraints cleared: PR #12325 (Open Graph) merged asc48d46d70, releasing these three files. #12242 stays queued BEHIND this card: it sharesapp/[lang]/page.tsxand the blog page, and it now also owns hoisting the/hero-cover-dark.pngliteral intolib/site.ts.⚠️ Standing condition for this round: production has not deployed since ~16:55 today (#12333), so nothing merged after that point can be verified against the live site. Verify against a local production build (next build && next start), and do not treat the live site as evidence for or against your change.Dev claim (os-dev subagent under the epic PM session, dispatched for this card)
- Session:
session_f9f0958b-ab68-46cc-801c-216aa7ee2107 - Branch:
claude/issue-12240-json-ld(pushed empty atcf99875eabefore the first edit — write-route probe green) - Worktree:
/Users/zhuangjianguo/Documents/GitHub/objectstack-issue-12240 - Base:
cf99875ea88a6a611353442e76c93de70727b74d - File surface:
apps/docs/app/[lang]/page.tsx,apps/docs/app/[lang]/docs/[[...slug]]/page.tsx,apps/docs/app/[lang]/blog/[[...slug]]/page.tsx
Standing conditions acknowledged:
app/page.tsxis shadowed (#12255) — the served homepage isapp/[lang]/page.tsx; production has not deployed since ~16:55 (#12333), so verification is against a localnext build && next startonly.- Session:
os-dev-report
{ "issue": 12240, "status": "done", "branch": "claude/issue-12240-json-ld", "pr": "https://github.com/objectstack-ai/objectstack/pull/12355", "premise_still_valid": true, "summary": "Zero JSON-LD existed under apps/docs (PM assumption 1 confirmed: the only grep hit was the word 'JSON-LD' in a lib/site.ts comment; positive control absoluteUrl returned 6 files). Five URL shapes confirmed by reading the route files (PM assumption 3 holds): / , /docs, /docs/**, /blog, /blog/**. Added a schema.org @graph to the three served page shapes from the same page object generateMetadata() reads: Organization + SoftwareSourceCode on /, Organization + TechArticle + BreadcrumbList on /docs and /docs/**, Organization + BlogPosting on /blog/**. /blog (the index) emits none — the card does not ask for it; stated rather than left to be found. Chose SoftwareSourceCode over SoftwareApplication because Google's Software App rich result REQUIRES offers/aggregateRating/review, so a SoftwareApplication node would report missing-required errors in the validator the acceptance criteria name, for a rich result this project can never be eligible for. dateModified reuses app/sitemap.ts's OWN output (lib/structured-data.tsx calls its default export once per process and indexes the array) rather than re-deriving git dates, so the JSON-LD value and the sitemap <lastmod> are the same object serialised the same way. DECLARED SURFACE EXPANSION: a fourth file, apps/docs/lib/structured-data.tsx, beyond the three declared page routes — one Organization identity instead of three, and one copy of the </script> escaping instead of three. Contention checked first: at the time of writing NO open PR in the repo touched apps/docs at all (enumerated over every open PR's file list), so a new path cannot collide with #12242. PM ASSUMPTION 2 IS PARTLY FALSIFIED: source.pageTree does carry a real ancestor chain, but for 17 of the 35 meta.json files under content/docs the ancestor arrives with a title and NO url, because fumadocs attaches a folder's index.mdx as folder.index only when meta.json does not list \"index\" in pages. 172 of 403 doc pages therefore ship a trail that skips its section (231 complete). I did NOT build the fallback the PM warned about: Google requires `item` on every crumb but the last, and reconstructing the URL in the consumer would make a content-config defect permanent and invisible. Filed as #12352 with the ablation; when it lands the trails complete with no code change. A hypothesis of mine that did NOT hold, recorded rather than dropped: I expected those 17 folder-index pages to be navigation orphans too — crawling all 408 sitemap URLs for inbound links found 4 orphans, none of them folder indexes (#12353). Per the standing condition, nothing was verified against objectstack.ai; all measurements are from a local next build && next start at 13791c71d.", "tests": "Union re-run on the final commit 13791c71d after the last push. Verdict lines quoted from the tools, not from $?.\n- pnpm --filter '@objectstack/docs^...' build -> os-verify-lock: VERDICT command-exit 0 (dependency closure first; 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.')\n- pnpm --filter @objectstack/docs typecheck -> VERDICT command-exit 0 (script echoed: 'fumadocs-mdx && next typegen && tsc --noEmit', so the filter matched — not a zero-match green)\n- npx next build (apps/docs) -> VERDICT command-exit 0; 1222 static pages, no warnings, no sitemap [sitemap] warning\n- eslint . --no-inline-config over the FULL repo -> VERDICT command-exit 0 in 46s, no narrowing. NOTE: plain `pnpm lint` ABORTS on this host with a V8 heap OOM (exit 134, '<--- Last few GCs --->'); that is NOT MEASURED, not a red — re-run with --max-old-space-size=8192 and the same unnarrowed population passes clean.\n- 5 families derived by `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (re-derived after the final commit, unchanged): check:docs-locale-catch-all, check:page-declaration-shape, check:published-files, check:test-source-alias, check:type-source-resolution — all exit 0, e.g. 'check-page-declaration-shape: OK — 34 page entries across 2190 sources ... all reach the kernel through a discoverable declaration'.\n- pnpm check:nul-bytes exit 0 ('check-nul-bytes: OK (scanned 6835 text file(s) ... no raw ASCII control bytes)') plus a direct grep -naP control-byte scan of the four changed files: no hits.\nRED FOUND AND FIXED INSIDE THE BRANCH, named because a diff hides it: the second commit is comments-only and it BROKE THE BUILD. A comment containing the glob content/docs/**/meta.json closes the block comment at */ — tsc reported 18 syntax errors (TS1443/TS1434/TS1161) and eslint a parse error at 46:29. Caught only because the gate union was re-run on the new head instead of skipped for 'just comments'. Fixed, union re-run green.\nABLATION (breadcrumb causation). Mutation and restore both proven on disk, and the tree was REBUILT on each leg. Target content/docs/data-modeling/meta.json; HEAD blob hash captured first and non-empty; trap '<restore>' EXIT INT TERM with an absolute repo root. Mutation: deleted-text count went 1 -> 0 and hash-object moved 25bf32b7 -> 0a2e03a6 (so the edit landed; not the editor's exit code). Rebuilt with next build (exit 0) and restarted next start, then read the RENDERED page: /docs/data-modeling/objects went from 'ObjectStack > Documentation > Object Metadata' (3 crumbs) to 'ObjectStack > Documentation > Data Modeling(/docs/data-modeling) > Object Metadata' (4). Control /docs/ai/agents, whose meta.json was untouched, stayed at 3 — so the effect is the line, not the rebuild. Restore leg: git checkout HEAD -- <path>, then git diff HEAD empty, git status clean, and hash-object back to 25bf32b7 == the HEAD blob; the tree was then rebuilt clean before any further measurement, so no later reading came off the mutated build. Predicted direction before running: the ablated section's trails lengthen while the control stays short — observed exactly that.\nEARLIER INSTRUMENTED BUILD, same discipline: a temporary console.warn probe in docsTrail (hash f444e51d, restored to 0c9be673 == HEAD blob, probe grep count back to 0) printed the raw fumadocs return: getBreadcrumbItems('/docs/data-modeling/objects') = [{\"name\":\"Data Modeling\"},{\"name\":\"Object Metadata\",\"url\":\"/docs/data-modeling/objects\"}] — the ancestor has a name and no url. That is the measurement behind the falsified assumption 2, not an inference from the tree's shape.\nVALIDATION METHOD — Google's Rich Results Test was NOT run and is not claimed: it is a hosted tool needing a public URL, and this branch is not deployed (#12333 also pins production to a pre-branch build). Instead, every JSON-LD block the site serves was checked field by field against schema.org property ranges and Google's documented required/recommended properties, over all 408 sitemap URLs: {\"pages\":408,\"withLd\":407,\"byType\":{\"Organization\":407,\"SoftwareSourceCode\":1,\"TechArticle\":403,\"BreadcrumbList\":403,\"BlogPosting\":3}}, failures: 0. Asserted per block: exactly one ld+json script; payload parses; @context is https://schema.org; NO raw '<' survives into the payload (the injection guard checked on output); every URL-valued property absolute and on https://objectstack.ai except the three declared external ones (sameAs, license, codeRepository); no relative URLs; every bare {\"@id\"} reference resolves inside that same page's @graph; Organization has name/url/logo/sameAs; TechArticle and BlogPosting have headline/image/author/publisher/mainEntityOfPage/url with url == the canonical and headline within Google's 110-char cap; datePublished and dateModified match ISO 8601; dateModified byte-equal to that page's <lastmod> in /sitemap.xml on all 406 dated pages (e.g. /docs/data-modeling/objects carries 2026-08-25T16:30:09.000Z in both); BreadcrumbList positions contiguous from 1, every entry with a name and an absolute item, last entry the page itself; SoftwareSourceCode.license is the Apache-2.0 URL rather than the bare SPDX id, because schema.org's license ranges over CreativeWork|URL and not Text.\nBREADCRUMB POPULATION on the final build: 403 docs URLs, 231 complete trails, 172 short — short ones concentrated in ai 8, api 11, automation 9, capabilities 10, concepts 5, data-modeling 17, deployment 11, getting-started 8, kernel 22, permissions 20, plugins 4, protocol 24, releases 8, ui 15.\nVERBATIM EMITTED BLOCKS for one page of each type are in the PR body and reproduced in the report JSON below.", "open_questions": [], "out_of_scope_findings": [ "filed as #12352: 17 of 35 content/docs meta.json files list \"index\" in pages, which detaches the folder index in fumadocs' page tree — 172 of 403 doc pages ship a breadcrumb that skips its section; ablation-proven, fix is one line per file but it also changes the sidebar", "filed as #12353: 4 doc pages have zero inbound links from any of the 408 sitemap pages (/docs/concepts/metadata-driven, /docs/kernel/contracts/auth-service, /docs/kernel/contracts/cache-service, /docs/protocol/backward-compatibility)", "filed as #12354: apps/docs/lib/layout.shared.tsx sets gitConfig.repo = 'framework', the pre-rename name — every nav/hero/edit link and now SoftwareSourceCode.codeRepository rides GitHub's rename redirect" ], "emitted_json_ld": { "/": { "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://objectstack.ai/#organization", "name": "ObjectStack", "url": "https://objectstack.ai/", "logo": "https://objectstack.ai/logo.svg", "sameAs": [ "https://github.com/objectstack-ai", "https://www.youtube.com/@objectstack" ] }, { "@type": "SoftwareSourceCode", "@id": "https://objectstack.ai/#software", "name": "ObjectStack", "url": "https://objectstack.ai/", "headline": "Metadata framework for AI-written apps", "description": "ObjectStack turns the whole app — data model, UI, workflows, permissions — into typed metadata: a complete CRM in under 150k tokens, one context window.", "image": "https://objectstack.ai/hero-cover-dark.png", "codeRepository": "https://github.com/objectstack-ai/framework", "programmingLanguage": "TypeScript", "runtimePlatform": "Node.js", "license": "https://www.apache.org/licenses/LICENSE-2.0", "author": { "@id": "https://objectstack.ai/#organization" }, "maintainer": { "@id": "https://objectstack.ai/#organization" } } ] }, "/docs/data-modeling/objects": { "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://objectstack.ai/#organization", "name": "ObjectStack", "url": "https://objectstack.ai/", "logo": "https://objectstack.ai/logo.svg", "sameAs": [ "https://github.com/objectstack-ai", "https://www.youtube.com/@objectstack" ] }, { "@type": "TechArticle", "@id": "https://objectstack.ai/docs/data-modeling/objects#article", "headline": "Object Metadata", "name": "Object Metadata", "description": "Define business entities with ObjectSchema — the core building block of every ObjectStack application", "url": "https://objectstack.ai/docs/data-modeling/objects", "mainEntityOfPage": "https://objectstack.ai/docs/data-modeling/objects", "inLanguage": "en", "image": "https://objectstack.ai/og/docs/data-modeling/objects/image.png", "dateModified": "2026-08-25T16:30:09.000Z", "author": { "@id": "https://objectstack.ai/#organization" }, "publisher": { "@id": "https://objectstack.ai/#organization" } }, { "@type": "BreadcrumbList", "@id": "https://objectstack.ai/docs/data-modeling/objects#breadcrumb", "itemListElement": [ { "@type": "ListItem", "position": 1, "name": "ObjectStack", "item": "https://objectstack.ai/" }, { "@type": "ListItem", "position": 2, "name": "Documentation", "item": "https://objectstack.ai/docs" }, { "@type": "ListItem", "position": 3, "name": "Object Metadata", "item": "https://objectstack.ai/docs/data-modeling/objects" } ] } ] }, "/blog/context-window-is-the-constraint": { "@context": "https://schema.org", "@graph": [ { "@type": "Organization", "@id": "https://objectstack.ai/#organization", "name": "ObjectStack", "url": "https://objectstack.ai/", "logo": "https://objectstack.ai/logo.svg", "sameAs": [ "https://github.com/objectstack-ai", "https://www.youtube.com/@objectstack" ] }, { "@type": "BlogPosting", "@id": "https://objectstack.ai/blog/context-window-is-the-constraint#article", "headline": "The Constraint Isn't Typing Speed. It's the Context Window.", "name": "The Constraint Isn't Typing Speed. It's the Context Window.", "description": "AI can write an enterprise app. Whether it can maintain one depends on whether the whole system fits in its head — which is an argument about the target format, not the model.", "url": "https://objectstack.ai/blog/context-window-is-the-constraint", "mainEntityOfPage": "https://objectstack.ai/blog/context-window-is-the-constraint", "inLanguage": "en", "image": "https://objectstack.ai/hero-cover-dark.png", "datePublished": "2026-07-17T00:00:00.000Z", "dateModified": "2026-08-18T01:17:36.000Z", "keywords": [ "ai", "architecture", "metadata", "positioning" ], "author": { "@type": "Organization", "name": "ObjectStack Team" }, "publisher": { "@id": "https://objectstack.ai/#organization" } } ] } } }ACCEPT — PR #12355, 4 files, +405/-2. The surface expansion is accepted; the reasoning below.
My assumption 2 was partly falsified, and the response to that is the best thing in this report.
source.pageTreedoes carry an ancestor chain, but for 17 of the 35meta.jsonfiles the ancestor arrives with a name and nourl— fumadocs attaches a folder'sindex.mdxasfolder.indexonly whenmeta.jsondoes not list"index"inpages. So 172 of 403 doc pages ship a trail that skips its own section. Google requiresitemon every crumb but the last.The dispatch said that if the tree did not give the chain cleanly, the finding was worth more than a hand-rolled fallback. It did not build the fallback. It filed #12352 with an ablation instead — and the ablation is causal, not correlational: it deleted one
"index"entry fromdata-modeling/meta.json, rebuilt, and watched/docs/data-modeling/objectsgo from 3 crumbs to 4 while an untouched control section stayed at 3. When #12352 lands, the trails complete with no code change here. A consumer-side URL reconstruction would have made a content-config defect permanent and invisible, which is exactly what this repo's contract-first directive forbids.dateModifiedis not a second source of truth —lib/structured-data.tsxcallsapp/sitemap.ts's own default export once per process and indexes the result, so the JSON-LD value and the<lastmod>are the same object serialised twice. Byte-equal on all 406 dated pages, checked. That was a hard constraint in the dispatch and it was met by construction rather than by coincidence.SoftwareSourceCodeoverSoftwareApplication, and the reason is right: Google's Software App rich result requiresoffers/aggregateRating/review, so aSoftwareApplicationnode would report missing-required errors in the very validator the card's acceptance names — for a rich result this project can never be eligible for.Validation claimed accurately. Google's Rich Results Test was not run and is not claimed — it needs a public URL and this branch is not deployed (and production is pinned, #12333). Instead all 408 pages were checked field by field against schema.org ranges and Google's documented required properties: 407 carry a block, 0 failures, and the assertions include the ones that actually bite — no raw
<surviving into the payload, every{"@id"}reference resolving inside its own@graph, breadcrumb positions contiguous from 1,licenseas the Apache-2.0 URL because schema.org'slicenseranges overCreativeWork|URLand notText. The blog index emits nothing, stated plainly rather than left to be discovered.Surface expansion accepted. A fourth file,
lib/structured-data.tsx, beyond the three page routes I declared — for oneOrganizationidentity instead of three copies, and one copy of the</script>escaping instead of three. Contention was checked before writing it (no open PR touchedapps/docsat that moment), which is the part that makes a declared expansion safe rather than merely announced.A build break found inside the branch, and named rather than buried: a comments-only commit broke the build, because a comment containing the glob
content/docs/**/meta.jsoncloses the block comment at*/— 18tscsyntax errors. Caught only because the gate union was re-run on a commit that "was just comments". Worth its own line in this repo, which already carries an ESLint rule for the sibling failure (an unterminated block comment swallowing code); this is the mirror image — a comment that terminates early.A hypothesis of the dev's own that did not hold, recorded rather than dropped: it expected the 17 folder-index pages to be navigation orphans too. Crawling all 408 sitemap URLs for inbound links found 4 orphans, none of them folder indexes (#12353).
⚠️ One thing this PR ships that is inherited-wrong, and the fix belongs elsewhere:codeRepositoryreadshttps://github.com/objectstack-ai/framework, fromgitConfiginlib/layout.shared.tsx. I verified it:objectstack-ai/frameworkandobjectstack-ai/objectstackreturn the same repository id (1136691870), so it is a rename redirect. Every nav link, hero button and edit link already rides that redirect, and now a machine-readable identity claim does too. ⛔ Not a local override here — fixinggitConfigfixes all of them at the producer, which is #12354. Promoting it now.
One-liner
The site emits no JSON-LD at all, so Google has to infer what ObjectStack is and where each doc page sits, and the docs are ineligible for breadcrumb rich results.
Expected
SoftwareApplication(orSoftwareSourceCode) +Organization— name, url, logo, licenseApache-2.0,sameAspointing at the GitHub org and the YouTube channel (https://www.youtube.com/@objectstack).TechArticle(headline, description,dateModifiedfrom the same git signal the sitemap uses) +BreadcrumbListderived fromsource.pageTree— breadcrumbs are the piece that actually shows up in results.BlogPosting.<script type="application/ld+json">in the layout/page, generated from the same page data as the metadata so the two can never disagree.Depends on the
metadataBasecard for absolute URLs.Acceptance
objectstack.aidateModifiedmatches the sitemap'slastModifiedfor the same pageSource
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: