Uh oh!
There was an error while loading. Please reload this page.
docs(content): propose a search-intent title rule, land it on the four pages with no sidebar cost - #12312
Merged
Merged
Conversation
The frontmatter title is the SERP <title>, the on-page <h1>, the sidebar label and the llms.txt heading — one string, four consumers. Lengthening it for search shortens nothing else, so this lands the rule only where it costs the navigation nothing: - content/docs/index.mdx, protocol/objectql/index.mdx and protocol/objectui/index.mdx have no sidebar entry of their own — their folder's meta.json title is what the tree shows. - protocol/objectui/record-alert.mdx was 64 characters with the site suffix, already over the 60-character budget; the new title is 52 and its sidebar label gets shorter, not longer. The rule and the full 180-row before/after table are in the PR body for the maintainer to judge. Nothing else is rewritten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
os-zhuang
marked this pull request as ready for review
August 26, 2026 00:08
Uh oh!
There was an error while loading. Please reload this page.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for freeto join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of #12237
⛔ Draft on purpose. This awaits the maintainer's judgement on the rule — please do not merge or queue it. Titles are maintainer-voice copy, so the rule and the complete before/after table are below and the diff is deliberately four files.
TL;DR
Three things the card did not know, all re-derived rather than inherited:
content/docs/references/**(generated, docs content: the 38 generated reference pages still render two<h1>— the heading comes from a spec JSDoc header via build-docs.ts #12249) and 9 undercontent/docs/releases/**(CLAUDE.md hard stop, docs content: 4 pages under content/docs/releases/ still render two or three<h1>— three of them need a cascading demotion, not a mechanical one #12250). Both are barred, and together they are 55% of the population. The card's headline median of 14 is dominated by the pages nobody may edit here.titleis simultaneously the SERP<title>, the on-page<h1>, the sidebar nav label and thellms.txtheading. The card made settling this a precondition for mass-editing. It settles negative.Because of (2), this PR lands the rule only on the four pages where it demonstrably costs the navigation nothing, and leaves the other 176 as a table to be judged.
1. Re-derived statistics
The card's numbers came from the same session and the same method that produced #12236's wrong
205/129/76, so everything here was measured again — frontmatter-aware, reading only the leading---block so atitle:line inside a body code fence can never be counted.The card's own figures hold. A naive
grep '^title:'over whole files returnsn=405 median=14 max=50 <=20: 327; the frontmatter-aware read returnsn=403 median=14 max=50 <=20: 325— exactly the card. The two extra hits aretitle:lines inside YAML fences. Unlike #12236, the method error did not move the headline. That is reported as a null result rather than dressed up as a catch.What the card's framing does hide is the split:
content/docs/**/*.mdx(the card)references/**— generated, #12249releases/**— hard stop, #12250Corroborated independently by two gates on this branch:
check-doc-frontmatterreportscontent/docs 403, andcheck-docs-single-h1reports180 page(s) under content/docs/ (2 subtree(s) excluded)— the same 180.Other measured facts:
protocol/objectui/record-alert.mdxat 64. Fixed in this PR.X — Yand 3 useX: Y.2. The sidebar-label finding (the card's stated precondition)
Checked against this repo's actual configuration (
fumadocs-core@16.14.4,apps/docs/source.config.ts,apps/docs/lib/source.ts). The answer is no.<title>page.data.title+%s | ObjectStackapp/[lang]/docs/[[...slug]]/page.tsxgenerateMetadata;app/layout.tsxmetadata.title.template<h1>page.data.title<DocsTitle>{page.data.title}</DocsTitle>page.data.titlefumadocs-corepage-tree builder,buildFile():name: title ?? pathToName(basename(path, extname(path)))llms.txtheadingpage.data.titleapps/docs/lib/source.tsgetLLMText():`# ${page.data.title}`One string, four consumers. And no second field exists to split them:
pageSchema(fumadocs-core/dist/source/schema.js) declares exactlytitle,description,icon,full,_openapi— and compiles toz.core.$strip, so an inventedsidebarTitle:in frontmatter is silently dropped, not rejected.source.config.tsusespageSchemaunextended (the blog collection does extend it, so the mechanism exists and is simply unused for docs).meta.json(metaSchema) has atitle, butbuildFolder()uses it for the folder's own label (node.name = metadata.title ?? node.index?.name). It cannot name a child page.meta.jsonpagesdoes accept a[Label](url)form, butresolveLink()emits a bare link node with no$ref— it dropsdescription,iconand the page↔tree binding, and the real page would need!-excluding to avoid appearing twice. That is a link mechanism, not a label mechanism.The supported fix is code, roughly ten lines:
pageSchema.extend({ sidebarTitle: z.string().optional() })inapps/docs/source.config.ts, plus apageTree.transformersentry inapps/docs/lib/source.tswhosefile(node)prefers it. That is inside epic #12243's territory but outside this card's declared file surface, andapps/docs/lib/source.tsis contended by cards in flight — so it is filed rather than taken here.The three-page exemption this PR uses instead
buildFolder()sets a folder's label tometadata.title ?? node.index?.name, and excludes the index page fromchildrenunlessmeta.jsonpagesnames"index"explicitly. So for a folder whosemeta.jsoncarries atitleand whosepagesomits"index", that index page's frontmatter title never appears in the sidebar.Enumerated across all 19 editable folders: exactly three pages qualify. 16 folders list
"index"inpages, which puts the page in the tree as an ordinary child.content/docs/index.mdxDocumentationisRoot, and rootmeta.jsonpagesomits it)content/docs/protocol/objectql/index.mdxObjectQL: The Data Protocol"Data Protocol"fromprotocol/objectql/meta.jsoncontent/docs/protocol/objectui/index.mdxObjectUI: The UI Protocol"UI Protocol"fromprotocol/objectui/meta.jsonThose last two are worth noticing on their own: the repo already demonstrates the split the card is asking for — a short nav label beside a longer page title — using the one mechanism that happens to work for folder index pages.
3. The proposed rule
| ObjectStacksuffix the rendered title lands in the 50–60 band. (The card said "50–60 including the suffix" but wrote its examples to 50–60 excluding it;ObjectStack documentation: metadata-driven app frameworkis 70 rendered andObject metadata: define objects, fields and relationshipsis 71. OnlyExpose actions as MCP tools for AI agents, at 55, obeys the box.)—, matching the rule as written and the 4 titles already using it. Where the qualifier is a genuine restatement,:also reads fine; the table uses—throughout for one pattern.getting-started/index.mdxkeepsWhat is ObjectStack?because the product name is the search query for that page.AI-written/AI-authored/AI-generatedonly, no fourth spelling.AI-writtenis the anchor for titles because the homepage title landed in docs(site): lead the homepage title with the category, cut the 614-char description to 152 #12284 asMetadata framework for AI-written apps.AI-builtis not zero. It appears 35 times across 18 files repo-wide (packages/objectql,packages/metadata-protocol,docs/adr, …). It is zero undercontent/docs/**andapps/docs/**, which is presumably the surface that was measured. The ruling still stands — no fourth spelling in titles — but it stands on taste, not on absence.Applied to all 180 rows the rule yields: median 54, min 50, max 60, 180/180 in the 50–60 band, zero over 60, zero duplicates.
4. What this PR actually changes: 4 files
Every file in the diff either has no sidebar exposure or shortens its sidebar label. Zero navigation regression, by construction rather than by judgement.
content/docs/index.mdxDocumentationDocumentation — build apps from metadatacontent/docs/protocol/objectql/index.mdxObjectQL: The Data ProtocolObjectQL — the data protocol specificationmeta.jsontitle winscontent/docs/protocol/objectui/index.mdxObjectUI: The UI ProtocolObjectUI — the UI protocol specificationmeta.jsontitle winscontent/docs/protocol/objectui/record-alert.mdxrecord:alert — Conditional Banners on Record Pagesrecord:alert — banners on record pagesEvery file is
1+/1-. Frontmatterdescriptionis untouched (#12238's card, same block, same files). Nothing underreferences/**orreleases/**.content/docs/index.mdxalso takes the free input from #12236: its demoted## ObjectStack Documentationconfirmed the page had a better wording available thanDocumentation.5. Acceptance boxes
pnpm check:doc-anchorsgreen —check-doc-anchors: 278 internal #fragment link(s) across 408 source file(s) all resolve to a real heading6. Rule applied to all 180 — the complete before/after table
Rows: 180. Lengths include the
| ObjectStacksuffix (14 chars) the root layout appends.✅marks the 4 rows this PR actually lands; the rest are proposals awaiting the ruling.content/docs/ai/actions-as-tools.mdxagents.mdxconnect-mcp.mdxindex.mdxknowledge-rag.mdxnatural-language-queries.mdxskills-reference.mdxskills.mdxtools.mdxcontent/docs/api/client-sdk.mdxdata-api.mdxdata-flow.mdxdeclarative-endpoints.mdxenvironment-routing.mdxerror-catalog.mdxerror-handling-client.mdxerror-handling-server.mdxindex.mdxmetadata-api.mdxplugin-endpoints.mdxwire-format.mdxcontent/docs/automation/approvals.mdxconnectors.mdxemail-templates.mdxflows.mdxhook-bodies.mdxhooks.mdxindex.mdxjobs.mdxwebhooks.mdxworkflows.mdxcontent/docs/build-without-code.mdxcontent/docs/capabilities/ai.mdxanalytics.mdxapprovals.mdxautomation.mdxdata.mdxforms.mdxindex.mdxintegrations.mdxpermissions.mdxrequest-template.mdxviews.mdxcontent/docs/concepts/architecture.mdxdesign-principles.mdxindex.mdxmetadata-driven.mdxmetadata-lifecycle.mdxnorth-star.mdxcontent/docs/data-modeling/analytics.mdxdrivers.mdxexternal-datasources.mdxfield-type-decision-tree.mdxfield-types.mdxfields.mdxformulas.mdximport-mappings.mdxindex.mdxindexing.mdxobject-extensions.mdxobjects.mdxqueries.mdxrelationships.mdxschema-design.mdxseed-data.mdxvalidation-rules.mdxvalidation.mdxcontent/docs/deployment/backup-restore.mdxcli.mdxenvironment-variables.mdxindex.mdxproduction-readiness.mdxpublish-and-preview.mdxseed-tenancy-repair.mdxself-hosting.mdxsingle-project-mode.mdxtenancy-modes.mdxtroubleshooting.mdxvalidating-metadata.mdxcontent/docs/getting-started/build-with-claude-code.mdxcommon-patterns.mdxexamples.mdxglossary.mdxhow-ai-development-works.mdxindex.mdxquick-reference.mdxquick-start.mdxyour-first-project.mdxcontent/docs/index.mdx✅content/docs/kernel/architecture.mdxcluster.mdxcontent/docs/kernel/contracts/auth-service.mdxcache-service.mdxdata-engine.mdxindex.mdxmetadata-service.mdxstorage-service.mdxcontent/docs/kernel/events.mdxindex.mdxcontent/docs/kernel/runtime-services/audit-service.mdxdata-service.mdxemail-service.mdxexamples.mdxindex.mdxqueue-service.mdxsettings-service.mdxsharing-service.mdxsms-service.mdxstorage-service.mdxversioning.mdxcontent/docs/kernel/services-checklist.mdxservices.mdxcontent/docs/permissions/access-matrix.mdxaccess-recipes.mdxadministrator-guide.mdxattachments-access.mdxauthentication.mdxauthorization.mdxcapabilities.mdxdelegated-administration.mdxexplain.mdxfield-level-security.mdxindex.mdxpermission-metadata.mdxpermission-sets.mdxpermissions-matrix.mdxpositions.mdxprofiles.mdxrecord-view-auditing.mdxrls.mdxsharing-rules.mdxsso.mdxsystem-context.mdxcontent/docs/plugins/adding-a-metadata-type.mdxanatomy.mdxdevelopment.mdxindex.mdxpackages.mdxcontent/docs/protocol/backward-compatibility.mdxdiagram.mdxindex.mdxcontent/docs/protocol/kernel/config-resolution.mdxerror-handling.mdxhttp-protocol.mdxi18n-standard.mdxindex.mdxlifecycle.mdxmetadata-service.mdxplugin-spec.mdxrealtime-protocol.mdxcontent/docs/protocol/knowledge.mdxcontent/docs/protocol/objectql/index.mdx✅query-syntax.mdxschema.mdxsecurity.mdxstate-machine.mdxtypes.mdxcontent/docs/protocol/objectui/actions.mdxconcept.mdxindex.mdx✅layout-dsl.mdxrecord-alert.mdx✅widget-contract.mdxcontent/docs/ui/actions.mdxapps.mdxaudience-based-interfaces.mdxcreate-vs-edit-form.mdxdashboards.mdxdoc-pages.mdxfield-grouping-and-order.mdxforms.mdxindex.mdxpages.mdxpublic-data-collection.mdxreact-pages.mdxreports.mdxsetup-app.mdxtranslations.mdxviews.mdxcontent/docs/upgrading.mdxVerification
All at pushed head
ed3afdfda,git status --porcelainempty.Gate family re-derived from the actual diff, not from the dispatch list:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack(4 paths vs merge base, three-dot). Its first run warnedSTALE TREE — 3 commit(s) behind origin/mainand named.github/workflows/lint.ymlas changed in that range, soorigin/mainwas merged in and the derivation re-run before anything was trusted.25 gate families run, every exit code captured before any pipe (each redirected to its own file,
$?read immediately), allexit=0, re-run in full at the final commited3afdfda:check:doc-anchors·check:doc-authoring·check:docs-audit-scope·check:docs-redirects·check:docs-single-h1·check:published-readme-links·check:react-page-adapter-contract·check:release-notes·check:role-word·check:cross-package-test-inputs·check:nul-bytes·check-ci-filter-parity·check-cross-package-test-inputs·check-doc-frontmatter·check-doc-route-spelling·check-docs-section-name·check-section-landing-index·@objectstack/lint check:doc-formula-expressions·@objectstack/lint check:doc-security-posture·@objectstack/spec check:docs·@objectstack/spec check:skill-examples·@objectstack/spec check:empty-state·@objectstack/spec check:liveness·@objectstack/spec check:strictness-ledger·@objectstack/spec check:variant-docsVerdict lines quoted rather than exit codes:
✅ check-doc-anchors: 278 internal #fragment link(s) across 408 source file(s) all resolve to a real heading✓ check-docs-single-h1: 180 page(s) under content/docs/ carry no body-level#heading (2 subtree(s) excluded)✓ check-doc-frontmatter: 2 content root(s) verified, each against its own floor — content/docs 403, content/blog 3.exit=1and are NOT recorded as failures — each printedPREREQUISITE NOT METor a missing-build-artifact banner and, in the gates' own words, "Nothing was measured … It is NOT a finding." They were re-run green afterpnpm exec turbo run build --filter=@objectstack/{formula,lint,spec,client,client-react}andpnpm --filter @objectstack/spec gen:schema. Recorded here so the first reading is not mistaken for a red that got quietly dropped.Two declared narrowings:
os-verify-lock.shran UNLOCKED. Every heavy command went through the entry point, which reports on this host:VERDICT command-exit 0 · UNLOCKED (declared) · no usableflockon this host, so the shared verify lock was NEVER taken and NOTHING was serialized · declare it in the PR body. macOS ships noflock; the script's own disclosure is pasted rather than paraphrased.pnpm lintnarrowed to the diff, with all three required measurements:eslint --print-config content/docs/index.mdxprintsundefined—.mdxis outside the lint population, and that is ESLint's answer, not an assumption;--format json: 4 files, 0 errors, 4 warnings, and all four warnings areFile ignored because no matching configuration was supplied;eslint.config.mjsstates at line 328 that this repo "never enables type-aware linting (noparserOptions.project, no typed@typescript-eslintrules) for ANY file", so a frontmatter change cannot move any untouched file's verdict.No changeset — docs content only, publishing nothing.
skip-changesetapplied.Out of scope, filed
apps/docschange that would unblock the remaining 176 rows of the table above.Generated by Claude Code