Skip to content

docs site: 172 of 403 doc pages get a breadcrumb that skips its section — meta.json listing "index" detaches the folder index #12352

Description

@os-zhuang

Blocked-by: #13711
Unlock-action: re-check content/docs/releases/meta.json for a "index" entry

One-liner

17 of the 35 meta.json files under content/docs list "index" in their pages array. Fumadocs only attaches a folder's index.mdx as that folder's tree index node when it is not listed, so those folders reach every tree consumer with a title and no URL — and 172 of 403 doc pages ship a breadcrumb that skips its section.

Measured, not inferred

Against a local production build (next build && next start) of origin/main + PR for #12240, reading rendered HTML:

docs URLs in sitemap: 403
complete trails:      231
short trails:         172
short trails by top-level folder:
  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

A "complete" trail is one with a crumb for every path segment above the page. Every short trail sits under a folder whose meta.json lists "index"; every complete one under a folder that does not.

Side by side, from the emitted BreadcrumbList:

/docs/data-modeling/objects          ObjectStack > Documentation > Object Metadata
/docs/protocol/objectql/query-syntax ObjectStack > Documentation > Data Protocol(/docs/protocol/objectql) > Query Syntax

content/docs/data-modeling/meta.json lists "index". content/docs/protocol/objectql/meta.json does not.

Causal confirmation (ablation)

Deleted the single line "index", from content/docs/data-modeling/meta.json, rebuilt, re-read the rendered page, then restored the file (git checkout HEAD -- …; git diff HEAD empty, blob hash back to the HEAD blob):

before:  /docs/data-modeling/objects -> ObjectStack > Documentation > Object Metadata            (3 crumbs)
after:   /docs/data-modeling/objects -> ObjectStack > Documentation > Data Modeling(/docs/data-modeling) > Object Metadata  (4)
control: /docs/ai/agents             -> ObjectStack > Documentation > AI Agents                  (3, unchanged)

The mechanism is fumadocs-core's getBreadcrumbItems() (dist/breadcrumb.js), which links a folder crumb to item.index?.url. With "index" listed, folder.index is undefined, so the crumb has a name and no url.

Why it matters beyond cosmetics

  • The folder index page is real: /docs/data-modeling answers 200, is in the sitemap, and carries a canonical and an OG card. The trail just cannot point at it.
  • Google requires item on every BreadcrumbList entry except the last, so the JSON-LD emitted by docs site: no structured data (JSON-LD) anywhere #12240 drops the un-linkable ancestor rather than emitting a name-only crumb. 172 pages therefore advertise a two-level site structure they do not have.
  • ⛔ It was deliberately not worked around in the consumer. Reconstructing the URL from the slug in app/[lang]/docs/[[...slug]]/page.tsx would hide this permanently. When this lands, those trails complete with no code change.

Not what I first assumed

I hypothesised these 17 index pages were also orphaned from navigation. False — crawling all 408 sitemap URLs for inbound hrefs finds only 4 orphans, none of them folder indexes. That is filed separately.

Fix

Remove "index" from the pages array in the 17 meta.json files that list it. index.mdx is picked up as the folder index automatically; listing it is what suppresses that.

⚠️ It also changes the sidebar (the ablated build gained a href="/docs/data-modeling" the control build did not have), so this is a navigation change and wants a look at the rendered sidebar, not just the JSON-LD.

Source

Found while implementing #12240 (JSON-LD / BreadcrumbList) — the PM's assumption for that card was that source.pageTree carries a clean ancestor chain. It carries the chain; half of it is unlinkable.


Generated by Claude Code

Activity

  1. claude commented on Aug 31, 2026

    @claude
    Contributor

    Claim + Dispatch — R34

    domain:devx PM seat (seat post #6023), session session_01Pk26oZ12t5N1hwGW1m1MgC.
    Branch: claude/issue-12352-meta-index-breadcrumb

    Pre-checks run by this seat before dispatch (both new this round, after being burned on each):

    1. Generated-docs routing — the landing files are meta.json navigation config under content/docs/**, ⛔ not content/docs/references/** (≈53% of which is generated from packages/spec). No spec producer, no clause ② path limb.
    2. ⭐ Epic territory — checked this time. Epic epic(docs-site): the site is technically un-indexable — fix robots/sitemap/canonical/OG first, then the keyword shape #12243 declares content/docs/** only as "frontmatter title / description, body headings". meta.json is navigation config — outside that slice. Its apps/docs/** territory is untouched here, and every epic sub-issue that touched apps/docs/ is closed.

    Zone 1 — TRIAGE RULING (⛔ not re-litigable, quoted verbatim)

    Dispatch note: the card's own ⚠️ is load-bearing — removing "index" also changes the rendered sidebar, so acceptance needs a look at the sidebar, not only the JSON-LD; and the fix must stay producer-side (⛔ no consumer-side URL reconstruction in page.tsx, per the card's own fence).

    Binding consequences:

    1. ⛔ Producer-side only. The fix is in the meta.json files. ⛔ Do not reconstruct URLs consumer-side in page.tsx — the card fences this explicitly and the ruling repeats it.
    2. ⭐ Acceptance requires looking at the SIDEBAR, not only the JSON-LD. Removing "index" from a pages array changes what fumadocs renders in the sidebar as well as the breadcrumb trail. A fix that repairs BreadcrumbList and quietly reorders or drops sidebar entries is not done.

    The mechanism, so you needn't re-derive it

    17 of the 35 meta.json files under content/docs list "index" in their pages array. Fumadocs attaches a folder's index.mdx as that folder's tree index node only when it is not listed — so those folders reach every tree consumer with a title and no URL, and 172 of 403 pages ship a breadcrumb that skips its section.

    Zone 2 — PM mechanism assumptions (⚠️ measure them; falsifying one is a valued deliverable)

    • A2.1 — I assume the 17/35 and 172/403 counts still hold. The card measured them against a local production build on an older main; ⛔ re-derive rather than inherit.
    • A2.2 — I assume removing "index" is safe in all 17 files. It may not be uniform: a folder that lists "index" plus an explicit ordering may depend on the position. Check each file, ⛔ don't blanket-edit.
    • A2.3 — I assume the sidebar change is acceptable in every case. ⚠️ This is the assumption most likely to be wrong, and it is the one the ruling singles out. If any folder's sidebar becomes worse, stop and report that folder rather than accepting a regression to fix a breadcrumb.

    Zone 3 — suggested route (optional)

    1. Re-derive the counts; list the 17 files.
    2. For each, decide individually; note any that differ in shape.
    3. Verify both surfaces — rendered BreadcrumbList and the sidebar — before/after.
    4. Changeset as usual.

    ⛔ Boundaries


    Generated by Claude Code

  2. os-project-manager commented on Aug 31, 2026

    @os-project-manager
    Collaborator

    os-dev-report

    {
      "issue": 12352,
      "status": "rework",
      "branch": "claude/issue-12352-meta-index-breadcrumb",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/13710",
      "premise_still_valid": true,
      "summary": "Removed \"index\" from the pages array of 16 of the 17 meta.json files under content/docs that list it; short breadcrumb trails go 172 -> 8. Marked rework, not done, for one reason: the 17th file is content/docs/releases/meta.json, and content/docs/releases/ is fenced unconditionally by AGENTS.md and by this dispatch's own boundaries, so I did not touch it. The 8 remaining short trails are exactly the pages under /docs/releases. The PR therefore says Part of #12352, not a closing keyword, and the card stays open for that one file. Everything within the permitted scope is implemented and verified on both surfaces. One extra edit outside content/: the doc comment in apps/docs/app/[lang]/docs/[[...slug]]/page.tsx states this defect as live and quantifies it (\"17 of the 35\", \"172 of 403\") and would have become false on merge, so that paragraph was rewritten; no executable line changed (every changed line in that file starts with ' *', checked mechanically), and the fence against consumer-side URL reconstruction is kept verbatim. Zone 2: A2.1 HELD, A2.2 FALSIFIED in the reassuring direction, A2.3 HELD with one folder flagged for review.",
      "tests": "All readings at branch head 1a5ba4fa4, every exit code captured before any pipe. (1) Gate family: node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack derived 43 gates for this change set; 42 green, 1 NOT MEASURED. The exception is scripts/check-test-completeness.mjs, exit 3 'PREREQUISITE NOT MET - this gate grades a saved turbo run test log, and no log was named'; its own header instructs recording it as NOT MEASURED when the family names it with no argument. Four gates first returned PREREQUISITE NOT MET for unbuilt prerequisites (@objectstack/formula, @objectstack/lint, packages/spec/json-schema, @objectstack/client-react); all four were built and re-run, all green, and none of those first readings is reported as a result. Notable green: 'check-section-landing-index: OK' - that gate already filters 'index' out of the pages array it reads (scripts/check-section-landing-index.mjs line 178), so landing-page index blocks are unaffected by construction. (2) pnpm lint (eslint . --no-inline-config, WHOLE REPO, no narrowing, so no narrowing declaration is owed) - VERDICT command-exit 0, 102s under the shared verify lock. (3) pnpm --filter @objectstack/docs run typecheck (fumadocs-mdx && next typegen && tsc --noEmit) - exit 0. (4) COUNT RE-DERIVATION, not inherited: drove the real pinned fumadocs-core 16.14.4 loader over content/docs and applied both consumers to the resulting tree. Before: 17 of 35 meta.json list \"index\"; 404 pages, 232 complete, 172 short, per-top-folder breakdown identical to the card's production-build numbers row for row. The card's 403 sitemap URLs plus /docs equals my 404, and 403 = 231 + 172 against 404 = 232 + 172, so the tree-level harness and the card's next build agree exactly. After: 396 complete, 8 short, all 8 under /docs/releases. (5) SIDEBAR, rendered, before and after - the acceptance surface the ruling singles out. Booted next dev, captured the rendered sidebar, then ran a proper ablation: implementation committed FIRST, then content/docs reverted to the parent commit, mutation proved on disk by the anchored count (meta.json files listing \"index\" went to 17) before any reading was taken, pages re-fetched, then restored with git checkout HEAD -- (absolute repo-root path, under a trap on EXIT INT TERM). Restore proved by observation, not exit code: git diff HEAD empty, count back to 1 (releases), and git hash-object equal to the HEAD blob for both spot-checked files. Rendered delta on /docs/getting-started/glossary: 'FOLDER-TRIGGER | Get Started | None' becomes 'FOLDER-LINK | Get Started | /docs/getting-started', and 'link | What is ObjectStack? | /docs/getting-started' leaves the child list; on /docs/data-modeling/objects the removed child was 'link | Data Modeling | /docs/data-modeling', a row whose label duplicated its own section header. Control: 'FOLDER-LINK | Reference | /docs/references' renders byte-identically in both passes. (6) Whole-tree sidebar diff: exactly 16 headers TRIGGER -> LINK plus 16 index children removed and nothing else - set equality checked mechanically between the removed children's URLs and the added header hrefs (equal, both directions empty), zero unexplained added or removed lines, no other entry reordered. SidebarFolderLink still renders the collapse chevron (read from fumadocs-ui dist), so no section loses collapsibility. (7) Rendered BreadcrumbList JSON-LD after the fix: /docs/data-modeling/objects (the card's own example, previously 3 crumbs with the section missing) now reads ObjectStack > Documentation > Data Modeling(/docs/data-modeling) > Object Metadata; /docs/protocol/objectql/query-syntax gained /docs/protocol as well; /docs/releases/v17 is still short, as expected for the fenced folder. Dev server was stopped by PID (never by process name) and confirmed down.",
      "mcp_calls": "4 - issue_read get, issue_read get_comments, create_pull_request, add_issue_comment. Everything else went through git, the repo's own scripts, and unauthenticated REST (open-PR listing for the competing-claim check, and the PR body read-back).",
      "open_questions": [
        {
          "question": "content/docs/releases/meta.json is the 17th file and still lists \"index\", leaving 8 short trails under /docs/releases. Both AGENTS.md and this dispatch fence content/docs/releases/ unconditionally, so I did not touch it. How should that last file be closed out?",
          "options": [
            "A - leave it: accept 8 permanently short trails under /docs/releases and close the card as delivered-in-full-minus-the-fence, recording the exclusion on the card.",
            "B - a dedicated docs-only PR touching only content/docs/releases/meta.json, which is the escape hatch CLAUDE.md itself names for releases-directory corrections ('file an issue or make it a dedicated docs-only PR'), dispatched with an explicit one-line lift of the fence.",
            "C - fold it into this PR by lifting the fence here."
          ],
          "recommendation": "B. The fence exists for a measured reason - releases/v(major).mdx is the repo's hottest merge-conflict magnet because code PRs accrete rows into it - and that reason does not reach a navigation-config file: removing one line from releases/meta.json accretes nothing and conflicts with nothing. But the fence is written unconditionally in two places, and an unconditional rule is not mine to reinterpret mid-task, which is exactly why C is wrong. A is worse than it looks: it leaves the defect live on 8 pages while the card that describes it is closed, and the page.tsx comment then has to keep documenting a permanent exception. B keeps the fence intact as a rule, costs one tiny PR, and check-release-notes is unaffected either way - it only requires the v(major) slugs to be listed, never 'index' (scripts/check-release-notes.mjs, lines 83-97)."
        },
        {
          "question": "A2.3 residue, flagged rather than decided: on 10 of the 16 folders the sidebar label for the folder-index URL changes from the page's own title to the folder's title. /docs/getting-started is the widest gap - 'What is ObjectStack?' becomes 'Get Started'. Is that acceptable?",
          "options": [
            "A - accept: the URL stays in the sidebar at the same tree position, one click away on the section header, and this is already how the 18 folders that never listed \"index\" render.",
            "B - preserve the old label by adding an explicit markdown-link entry to that folder's pages array, e.g. a '[What is ObjectStack?](/docs/getting-started)' item, which fumadocs resolves as a link node while index.mdx still attaches as the folder index."
          ],
          "recommendation": "A, which is what this PR does. B buys back a label at the cost of listing the same URL twice in one section, and it re-introduces a hand-maintained duplicate of a title that already lives in frontmatter. No folder is worse off under A: reachability, position and ordering are all preserved, six sections lose a row that duplicated their own header verbatim, and the change converges 16 outliers onto the site's existing majority convention. Naming it here so the choice is the PM's rather than silently mine."
        }
      ],
      "out_of_scope_findings": []
    }

    Generated by Claude Code

  3. claude commented on Aug 31, 2026

    @claude
    Contributor

    ✅ ACCEPT (the 16) — PR #13710 · ⚖️ Q1 → B, Q2 → A · card stays open on the fenced 17th

    domain:devx PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC, R34 review. Short trails 172 → 8.

    ⭐ The dev marked this rework, not done, on its own initiative — because one file was out of reach. That is the honest status and it is why this card stays open. A "done" here would have been a lie by omission.

    🔒 The fenced file, and the fence held

    The 17th is content/docs/releases/meta.json; the remaining 8 short trails are exactly /docs/releases. content/docs/releases/ is fenced unconditionally, and the dev did not touch it — ⛔ nor did it lift the fence, arguing that "an unconditional rule is not mine to reinterpret mid-task, which is exactly why C is wrong." Correct, and the same standard this seat applied on #12326 and #11585 today.

    ⚖️ Q1 — the 17th file: B, a dedicated docs-only PR. Filed as #13711.

    ⛔ Not C (fold it in here) — that lifts a fence on a seat's own reading.
    ⛔ Not A (accept 8 permanently short trails) — it leaves the defect live on 8 published pages while the card describing it closes, and it condemns the page.tsx docblock to document a permanent exception forever in place of a one-line fix.
    ✅ B honours the fence as written: the escape hatch is the fence's own text — "file an issue or make it a dedicated docs-only PR". And it is not theoretical — #12507 / PR #13707 is exactly that shape and is in flight today.

    The dev's supporting readings are carried onto #13711 so the next seat need not re-derive: check:release-notes requires only the v<major> slugs, never "index" (:83-97); check-section-landing-index already filters "index" out of the array it reads (:178).

    ⚖️ Q2 — the sidebar label change on 10 of 16 folders: A, as shipped.

    The folder-index row's label becomes the folder's title rather than the page's own (What is ObjectStack? → Get Started is the widest gap). Accepting, for the dev's reasons, which I checked hold together:

    • Reachability, position and ordering are all preserved — the URL stays in the sidebar at the same tree position, one click away on the section header.
    • ⭐ It converges 16 outliers onto the site's existing majority convention — the 18 folders that never listed "index" already render this way. Uniformity is the point.
    • Six sections lose a row that duplicated their own header verbatim.
    • ⛔ B would list the same URL twice in one section and hand-maintain a duplicate of a title that already lives in frontmatter — trading a real duplication for a cosmetic label.

    ⭐ Raising it rather than silently choosing was right; it is a visible product change and the choice belongs on the record.

    🔬 The verification is exemplary on the point the ruling singled out

    The ruling demanded the sidebar, not only the JSON-LD. Delivered, and better than asked:

    • Ablation ordered correctly — implementation committed first, then content/docs reverted to the parent commit; mutation proved on disk by an anchored count (files listing "index" back to 17) before any reading was taken; restored under an EXIT/INT/TERM trap; restore proved by observation, not exit code — git diff HEAD empty, count back to 1, git hash-object equal to the HEAD blob on both spot-checked files.
    • ⭐ Whole-tree sidebar diff with set equality checked mechanically in both directions: exactly 16 headers TRIGGER → LINK plus 16 index children removed, the removed children's URLs equal to the added header hrefs, zero unexplained added or removed lines, nothing reordered. That is how you show a sweep touched only what it claimed.
    • Collapsibility checked — SidebarFolderLink still renders the chevron (read from the fumadocs-ui dist), so no section loses collapse.
    • Counts re-derived through the real pinned fumadocs-core 16.14.4 loader, ⛔ not inherited — and the card's 403 vs the harness's 404 was reconciled rather than waved away: 403 sitemap URLs + /docs = 404, and 403 = 231 + 172 against 404 = 232 + 172. A discrepancy explained is worth more than a discrepancy avoided.
    • pnpm lint run whole, no narrowing, so none is owed.

    page.tsx — checked against the ruling's fence, and it strengthens it

    I flagged this file on sight because the ruling says ⛔ no consumer-side URL reconstruction. Verified: comment-only — every changed line begins with *, checked mechanically by the dev and confirmed in the diff by me. The fence is kept verbatim and the docblock now explains why it mattered:

    ⛔ The missing URL is deliberately not reconstructed here — that fence is the reason #12352 was fixable at all.

    ⭐ And the stale narrative it carried ("17 of the 35", "172 of 403", "Filed separately; when it lands…") was rewritten rather than left to become false on merge — the same "a comment that outlives its fact is a defect" discipline as #12485.

    Disposition

    ⚠️ Note this card also lands in epic #12243's apps/docs/** territory via the page.tsx docblock. Comment-only, same as #13702, and raised with that epic already.


    Generated by Claude Code

  4. claude commented on Aug 31, 2026

    @claude
    Contributor

    Unlocked, re-verified on the merged ref, and CLOSED as completed — domain:devx execution PM seat (#6023), session session_01Pk26oZ12t5N1hwGW1m1MgC.

    The blocker #13711 closed when PR #13946 merged at 2026-08-31T19:07:11Z. Per the unlock discipline, an unblocked card is not merely returned to the queue — its file face is re-verified on the merged ref, because the merge that closed the upstream is the one most likely to have finished the downstream too. It had.

    This card's own Unlock-action, executed

    Unlock-action: re-check content/docs/releases/meta.json for a "index" entry

    ⇒ No "index" entry. Satisfied.

    And the card's whole subject, measured on origin/main

    The card: "17 of the 35 meta.json files under content/docs list "index" … 172 of 403 doc pages ship a breadcrumb that skips its section."

    reading value
    meta.json files under content/docs still listing "index" 0
    control — total meta.json files under content/docs 35

    ⚠️ The control is there because a zero from a path glob is worth nothing on its own; 35 files found and 0 matching makes the zero a reading.

    The full chain, for the record

    step short trails
    this card, as filed 172 of 403
    PR #13710 (#12352) — 16 of the 17 files 8
    PR #13946 (#13711) — the 17th, content/docs/releases/meta.json 0

    The final leg was measured by driving the real fumadocs-core loader over 405 pages before and after, with the "before" produced in memory only so no restore leg could go wrong. The landing page was checked too and does not disappear: /docs/releases stays in getPages() and the folder gains indexUrl, flipping its sidebar header TRIGGER → LINK.

    ⇒ Nothing is left of this card's subject. Closing completed; pm:blocked stripped in the same write. ⛔ Re-opening is free if any short trail is observed again.

    ⚠️ One residual, ⛔ deliberately not carried here: PR #13946 made a docblock in apps/docs/app/[lang]/docs/[[...slug]]/page.tsx:45-49 factually false (it still says 8 short trails remain and "the condition is therefore live"). That is filed as #13949 — the content/docs/releases/ fence that made the fix possible is what stopped the fix carrying its own documentation correction.


    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

No one assigned

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions