Skip to content

docs: contrast, phone table affordance, FAQ headings, title-weighted per-locale search, consistency pass (#301) - #310

Merged
hotlong merged 6 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1
Oct 6, 2026
Merged

hotlong merged 6 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #301

Docs polish from the 2026-10-06 audit, plus the two comments on the card (search cold start and post-deploy smoke from #296; three configure/ai citations and the empty Releases line from #297). Every item is fixed below, or answered in one line where it is not.

1. Contrast

  • Light-mode muted text: --color-fd-muted-foreground is set to hsl(0 0% 40%) in an @theme block in apps/docs/app/global.css. The neutral theme's dark value is a plain .dark rule, so it is untouched.
  • Code comments: shiki colorReplacements in apps/docs/source.config.ts, spread over fumadocs' own rehypeCodeDefaultOptions. #6a737d becomes #8b949e in github-dark and #57606a in github-light. #6a737d is the comment colour in both themes, and it colours nothing else in either.
Surface Mode Before After
Sidebar links light 4.20 5.08
Table of contents light 4.35 5.27
Search button light 4.12 4.99
Description under the title light 4.35 5.27
Code comments dark 3.65 5.72
Code comments light 4.26 5.66
Muted text, all four surfaces dark 6.08–8.86 unchanged

How these were measured: Chromium, each element's computed colour composited over its real background, on /docs/quickstart and /docs/configure/ai at 1440 and 390.

Light-mode code comments: the card does not name them. They measured 4.26, the same defect class, fixed by the same mechanism on the same line, so they are fixed in place here.

2. License Editions table on phones

mdx-components.tsx overrides table. It keeps fumadocs' own wrapper and classes and adds a docs-table class. global.css gives that class two affordances:

  1. A 6 px scrollbar that is always drawn. It is styled with ::-webkit-scrollbar and deliberately sets no scrollbar-width or scrollbar-color, because Chrome 121+ drops ::-webkit-scrollbar when either is set. Firefox gets scrollbar-width: thin under @supports not selector(::-webkit-scrollbar).
  2. A 40 px trailing fade driven by scroll position. It narrows over the last tenth of the scroll and is gone at the end. This is the same approach as www Consolidate the three stalled Dependabot bumps into one lockfile regeneration #157.

Measured under mobile emulation:

  • Scrollbar: 0 px before (an overlay scrollbar, drawn only mid-swipe), 6 px after, at both 390 and 360.
  • Mask at scroll start: calc(100% - 40px).
  • Mask at scroll end: 100%, so nothing is faded.
  • Desktop: at 1440 the table does not overflow, its scroll timeline is inactive, and the mask stays fully opaque.
  • Page width: the page's scroll width still equals the viewport.

3. FAQ formatting

The FAQ page has 35 questions and the License FAQ has 7. Each question is now a ### heading, with its answer as a paragraph of its own. The Q: / A: prefixes are gone. The questions are now linkable and appear in the table of contents.

After normalising the formatting, the only text changes are the license-validation sentence in one answer on each page (item 5).

4. Search (apps/docs/app/api/search/route.ts)

Ranking. Orama ranks a page's title, its headings and its paragraphs on one shared content field. A per-property boost cannot tell them apart, so a custom sortBy weights the hits instead: title 4, heading 2, text 1.

  • "permissions": /docs/configure/permissions now ranks first. Before, it was outside the top 8.
  • "air-gapped": /docs/deploy/air-gapped now ranks first. Before, it was third.
  • Pages found first by their own title (English): 30 of 79 before, 76 of 79 after. The other three (Approvals, Dashboards, Notifications) share their title with a second page.

A side finding. Before this change, title-versus-heading ties were broken by indexing order. fumadocs-mdx does not keep that order stable between builds: two builds of one tree ranked the same ties differently, in 27 of 142 responses.

Cold start. The index for a locale is now built on that locale's first search; before, the first search in any locale built all eight. This is the "one locale lazily" option from the #296 comment. These are first-search times after boot, measured interleaved on one box:

Runtime, first search main @ 601bb37 This branch
next start, en 8.72–10.14 s 0.63–0.82 s
next start, ja 8.95–9.67 s 1.23–1.37 s
workerd (opennextjs-cloudflare preview), en 5.11 s 0.81 s
workerd, ja 4.67 s 1.04 s
  • Each further locale's first search: 0.48–2.21 s.
  • A warm search: 23–222 ms, both before and after.
  • Worker bundle (wrangler deploy --dry-run): 47016.28 KiB, against a budget of 61440 KiB.

Why the index is not built at build time. fumadocs' build-time export (staticGET) is client-side search:

  • every locale's index ships to the browser;
  • /api/search becomes a static file that no longer answers ?query=, and both check-search-locales.mjs and the smoke test call that;
  • the Intl.Segmenter CJK tokenizer would have to be rebuilt in the browser bundle;
  • the client needs type: 'static' set on RootProvider in root-provider.tsx.

Per-locale lazy indexing gets most of the cold-start win from route.ts alone.

Parity. The per-locale index is built from public fumadocs APIs (createSearchAPI, findPath). Breadcrumbs are built the same way createFromSource builds them. With all weights set to 1, it was compared byte for byte against createFromSource in one process:

  • 165 requests: 11 locale values (including missing, empty and unknown) × 15 queries (including empty, CJK and a nonce);
  • 6299 hits compared;
  • 0 differences.

Gate. check-search-locales.mjs gains a rule, own-title-buried: a page's own title must rank it within the first 3 pages. The self-test has 11 cases, and all 7 rules can fire.

  • Run against main's built route: red, with 56 own-title-buried findings.
  • Run against this branch: green. Per locale, en ranks 76 of its 79 pages first, zh-Hans and zh-Hant 47 of 48, and ja, de, es, fr and ko 24 of 24.

Post-deploy smoke. smoke-docs.mjs now sends GET /api/search?locale=…&query=permissions for every locale in lib/i18n.ts. It reads that file as text, so the smoke stays install-free. Each response must be a 200 JSON array and must not be empty.

  • Nonce control: one extra request for a nonce must come back empty (search-negative-control-passed). Without it, a cache that ignored the query string would read as green.
  • Page negative control: unchanged.
  • Self-test: 4 new rules, each with fixtures. All 16 rules are shown able to fail, plus one whole-run case that is red with a query-ignoring search and green with a working one.
  • Ablation: with the nonce rule disabled, the self-test goes red (2 cases). The restored file's blob hash equals HEAD's.
  • Against a local workerd preview of this branch's Worker: all pages pass. Search answers in 8 locales with 65–144 hits, 0.50–0.95 s each, and the nonce finds 0.

The ci.yml comment that counted "four fetches" now names the search requests.

5. Consistency

  • Data residency. One table, kept in reference/security.mdx#data-residency. It gains the rows only the other copies had: the compiled app definition, the runtime itself, and "secrets". index.mdx and architecture.mdx now link to it.
  • License-validation sentence. It now reads identically in index.mdx, architecture.mdx, the License FAQ and the FAQ: "Self-managed ObjectOS validates its license online; Enterprise air-gapped licenses validate offline." security.mdx keeps its own phrasing, in context, as the canonical page.
  • defineObject. It no longer appears anywhere in the English docs.
    • reference/rest-api.mdx and configure/permissions/record-access.mdx now use ObjectSchema.create (from @objectstack/spec/data).
    • Source: objectstack @ 8a399b2b, content/docs/data-modeling/objects.mdx:15 (ObjectSchema.create, 58 uses in 31 pages) and packages/spec/src/kernel/manifest.zod.ts:386. The latter says the platform "deliberately does NOT provide" a generic defineObject factory.
    • In the REST sample, apiMethods and apiEnabled now sit under enable. That is where ObjectSchema declares them, and ObjectSchema.create rejects unknown top-level keys.
    • The REST page's "Allowed values" now lists only the six primitives: get, list, create, update, delete, bulk. It says the eight older values are retired and are stripped at parse with a warning (object.zod.ts:25-41).
  • build/interface/views.mdx. The two declaration samples used defineObject with an object-level view: { list, form, listViews, formViews }. They now show the form ObjectStack documents: views declared beside the object in a defineView container, registered on the stack with views: [...].
    • The container holds a default list, named listViews, a default form and named formViews. Each view is bound through data: { provider: 'object', object: … }, and every list view keeps a top-level columns.
    • Source: objectstack @ 8a399b2b, content/docs/ui/views.mdx:9-73, whose example is os:check-compiled there, together with packages/spec/src/ui/view.zod.ts:4749 (ViewSchema) and :4845 (defineView). KanbanConfigSchema.columns is required (view.zod.ts:1809).
    • Both samples, as published on the page, parse with @objectstack/spec 17.6.0 from npm.
    • Controls on the same install: the old object-level view key is refused by ObjectSchema.create ("view is not an ObjectSchema field"); there is no defineObject export; top-level apiMethods is refused; a list view without columns is refused. Declaring apiMethods: ['get','list','search','export'] parses to ['get','list'] with one warning.
    • The page links to ObjectStack's View metadata page.
    • Locale siblings stay, under ruling A: this is a code-shape correction, and declaring views is still asserted. The same holds for the "Allowed values" change, since the legacy values still parse and the reachable operations do not change.
  • Spelling: "license". AGENTS.md has no spelling rule. The English corpus is American on every other pair (organization 83:1, behavior 26:7, catalog 35:1), and the page title, URL and env vars all say "license".
    • 86 replacements in 14 English pages, MDX comments included; no locale file is touched.
    • Changed heading anchors: deploy/docker#licence became #license, and its one inbound link is updated. Three more headings changed slug in air-gapped, kubernetes and environment-variables. No inbound link to any of them was found in this repo, www or objectstack.
  • Configure. The configure/index.mdx title changes from "Administration" to "Configure". That matches the sidebar group, the URL, the 7 existing [Configure](/docs/configure) links, and how operate/ names its index page. The three "Administration" link texts are updated.
  • Glossary. Artifact is moved into alphabetical order. Two entries are added: AI seat, from License & Pricing, and Position, from configure/permissions/positions. The editions are left until [Decision] One name for the self-managed edition: "ObjectOS Enterprise" (positioning, 8 pages) vs "ObjectOS Self-Managed" with Business single-node and Enterprise under it (License & Pricing, Deploy) #295.
  • Logo. Decided: the logo goes to the docs home in the reader's locale (/docs, /zh-Hans/docs, …). A docs reader clicking it expects the docs start, not an English marketing page. www.objectos.ai stays one click away as an icon link beside GitHub; its label is a host name, so it needs no ui-text entry.
  • llms.txt. The Other Languages example is now derived: the first page in navigation order that has a real translation in the first other locale. Today that is /zh-Hans/docs/use, which renders 使用 ObjectOS; /zh-Hans/docs/quickstart was an English fallback.
  • configure/ai citations.
  • Releases line. resources/support.mdx now points at Release notes and at objectstack-ai/objectstack → Releases, which publishes (@objectstack/runtime@17.6.0 is there). objectstack-ai/objectos has 0 releases. resources/changelog.mdx pointed readers at the same empty feed twice, so it gets the same fix in place.

Branch and rebase

Head: ff/issue-301 @ 911a174. It fast-forwards from the seat branch claude/pm-dispatch-objectos-ju9td1 @ 7b4fd19 (#309's head):

  • Commits: a merge of origin/main @ a24b39d, whose tree is identical (git diff origin/main is empty after the merge), followed by this PR's four commits, cherry-picked.
  • Same patch: its diff against origin/main is byte-identical to the diff of staged/issue-301 @ 2e474fd, the same four commits rebased onto a24b39d (sha256 043d7c07… for both). Both tips have the tree 263e3709….

Rebasing onto #167 (a24b39d) produced the two predicted conflicts. Both lines were kept in each, and #167's seoTitle values were aligned to this PR:

  • configure/index.mdx: title: Configure + seoTitle: "Configure: Users, Access and Settings" (was "Administration: …").
  • deploy/air-gapped.mdx: seoTitle: "Air-gapped Deployment: License Mode and Settings" (was "Licence") + the description with "license".

Re-checked: no other English seoTitle contains "Licence" or "Administration", and no English page contains "licence". "Licensed" and "Licensing" are spelled the same in both variants.

Acceptance notes

Observations. None of these is filed by this PR.

  • The rest of build/interface/views.mdx. The per-type fragments further down the page (Kanban, Calendar, Gantt, …) still omit the top-level columns that every list view requires. The "Common list options" fragment sets fontWeight: 600, where the spec wants a string. Wrapped as list views with data, each of these is refused by @objectstack/spec 17.6.0. They are not touched here.
  • build/automation/flows.mdx samples. They use action: 'send_email', and the Email bullet calls it "the send_email action". FlowNodeAction has no such value. This is a read-only inference; it is not verified which schema the action key belongs to.
  • Search tie order. It depends on fumadocs-mdx's generated import order, which differs between builds. The weights settle title-versus-heading ties; ties between documents of the same kind still follow build order.
  • AGENTS.md coverage count. It says "62 of 79" Simplified pages. main has 50, and this branch has 48.
  • Other spelling pairs. colour/color, behaviour/behavior and catalogue/catalog are also mixed. The card scoped spelling to licence/license.
  • Section index titles. "Deploy" / "Deployment" and "Use" / "Using ObjectOS" have the same mismatch as Configure had. The card named Configure only.
  • Local verification trap. next start -H 127.0.0.1 loops forever (307) on prefix-less English routes, because the middleware's rewrite to localhost is treated as an external proxy. Bind without -H when verifying locally.

Verification (at 2e474fd; ff/issue-301 @ 911a174 has the identical tree)

Gate Verdict
pnpm turbo run type-check --continue --force exit 0, 1 successful
NEXT_PRIVATE_STANDALONE=true pnpm turbo run build --force exit 0, 1 successful
pnpm turbo run test --force exit 0, "✓ 10 self-test(s) passed"
gen-zh-hant.mjs --check "✓ zh-Hant: 60 generated file(s) match the zh-Hans sources byte for byte."
check-locale-surface.mjs exit 0
check-positioning.mjs exit 0, "4 copies equal their constants; … no stale sentence in 79 English sources"
check-search-locales.mjs exit 0, "all 8 locales … within the first 3 pages, and find nothing for a nonce"
check-translation-ownership.mjs, workflow argv (git diff --name-status --no-renames origin/main...HEAD), actor hotlong, bot login set exit 0, "46 file(s) changed: 14 translation artifact(s) deleted, none added or modified."
The same, with the bot as actor (control) exit 1
check-translations.mjs "✓ translations gate passed". Stale counts are reported only. On main @ a24b39d: zh-Hans 49, the other locales 26. On this branch: zh-Hans 47, the other locales 24
check-translation-output.mjs --files … "✓ translation output gate passed (256 pre-existing finding(s) reported)". main @ a24b39d reports 261 on its own; most come from #167's locale seoTitle lines. This branch reports 5 fewer, because of the deleted siblings, and adds the non-blocking fence-count differences in the views siblings
check-node-floor.mjs · half-states --self-test exit 0 · "1551 cases pass"
smoke-docs.mjs --base against a local opennextjs-cloudflare preview (run at 8dc4d10; later commits change MDX content and frontmatter only) exit 0, "4 page(s) rendered …, negative control demonstrated red, search answered in 8 locale(s) and found nothing for a nonce"

Screenshots, before and after, light and dark, at 1440 and 390: a page with sidebar and TOC (quickstart-*), the License table (license-*, plus 360 and scrolled-to-end variants), the FAQ (faq-*), the search dialog with "permissions" (search-permissions-*), and code comments (code-comments-*). Also the mobile-emulated table bottom with the drawn scrollbar (license-*-table-bottom-mobile). The before search-permissions-light-1440 shot caught the dialog still empty 2.5 s into the first search: that is the cold start, before this change.


Generated by Claude Code

objectstack-fleet Bot and others added 6 commits October 6, 2026 10:36
…ds or fewer

Adds a `seoTitle:` frontmatter line to every English page whose built
`<title>` read as one or two words plus the ` | ObjectOS` suffix, so the
tab/result title carries the terms a reader searches for while the H1,
sidebar and breadcrumb keep the short noun (#166's mechanism).

- 67 pages: each gains exactly one line; no `title`, `description` or
  body line changes.
- Every title leads with the page's own specific term, lifted from its
  description and headings, and renders at 52–60 characters including the
  suffix (measured on the built HTML).
- The three duplicated titles (Approvals, Dashboards, Notifications —
  each used by a build/configure page and a use page) are now distinct.
- English only. Locale siblings are translation artifacts that a
  non-translator commit may not modify (AGENTS.md, Translation workflow;
  check-translation-ownership.mjs); the translation pass carries the new
  key over, and the output report lists the 163 siblings now missing it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…AQ headings, title-weighted per-locale search, consistency pass

- Light-mode muted foreground to hsl(0 0% 40%); code comments recoloured in
  both shiki themes.
- Tables get an always-drawn scrollbar and a scroll-driven trailing fade.
- FAQ and License FAQ questions become headings.
- /api/search builds one locale's index on that locale's first search and
  weights title > heading > text; check-search-locales gains own-title-buried;
  smoke-docs asks /api/search in every locale with a nonce control.
- Consistency: one data-residency table, one license-validation sentence,
  ObjectSchema.create, "license" spelling, Configure title, glossary order plus
  AI seat and Position, logo to the docs home, a translated llms.txt example,
  three unsourced configure/ai claims removed (with their locale siblings),
  release-following lines pointed at a populated feed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ix apiMethods primitives

The Views page declared views inside a defineObject call under an
object-level `view` key. ObjectStack provides no defineObject, and
ObjectSchema.create rejects `view` as an unknown key. Its views are a
defineView container ({ list, listViews, form, formViews }, each view
bound through `data`) registered on the stack with `views: [...]`, which
both samples now show; both parse with @objectstack/spec 17.6.0.

The REST API page's allowed apiMethods values drop the eight retired ones
the spec strips at parse.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 14:19
@hotlong
hotlong merged commit 5d2f837 into main Oct 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants