Skip to content

Docs page improvements: one trail, visible links, search-first control plane - #986

Merged
blove merged 27 commits into
mainfrom
blove/docs-page-improvements-66cf88
Sep 3, 2026
Merged

Docs page improvements: one trail, visible links, search-first control plane#986
blove merged 27 commits into
mainfrom
blove/docs-page-improvements-66cf88

Conversation

@blove

@blove blove commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Six improvements to the /docs experience, plus two pieces of dead code this work exposed.

  • Run on the docs index now goes somewhere. The rail item was hardcoded disabled ("no workspace capability") — honest on a docs-only page, a dead control on /docs, which is not a capability page at all. It now links the canonical example. The href is resolved through the registry rather than written as a path, because /workspace/langgraph/streaming 404s: that capability publishes a docsPath, so its canonical destination is /docs/langgraph/guides/streaming?mode=run.
  • The Scope card is replaced by search. It restated the breadcrumb trail. A full-width search trigger takes its slot, wired to the existing openSearch() handoff (so the mobile drawer still works). The now-duplicate Search icon leaves the Actions bar, and Actions renders only when the library publishes a demoUrl.
  • One breadcrumb trail instead of four. A doc page rendered the same trail four times: the shell's manifest-derived mono label (which read Ag Ui / Getting Started / Overview — that casing and "Overview" are manifest facts, not docs facts), DocsBreadcrumb, DocsPageHeader's lib · section label, and the Scope card. WorkspaceShell gains an optional contextTrail; absent it, cockpit's derived label is byte-identical. DocsBreadcrumb is deleted.
  • Docs prose links are visible again — repo-wide. The reported bug was callout links, but the cause was broader: MdxRenderer carries prose prose-slate and sets --tw-prose-links, yet Tailwind Typography is not active in this app. A stylesheet walk in the loaded page found zero rules matching .prose, so the variable was set and nothing read it. Every docs link rendered as plain body text (measured rgb(28,28,28), no decoration, weight 400). Explicit .docs-prose rules now own it, callout links take the callout's tone, and absolute hrefs get an off-site glyph. Component anchors opt out via a new data-mdx-chrome attribute.
  • CTA buttons in the callouts that exist to send you somewhere. Five callouts across AG-UI and the three runtimes were prose wrapped around a demo link. They now lead with a button. Adds the missing LangGraph one — AG-UI's introduction advertised a live demo and the flagship adapter's did not. The authoring rule (CTAs only in "go do this" callouts, never explanations or cautions) is documented on the component so the other 146 callouts don't sprout buttons.
  • The search footer is on every docs content page. And it became a real button: "Press ⌘K" was static text, unactionable on a device with no ⌘K.

Found and removed along the way: a pageTitle prop that nothing read (a required field on DocsControlPlaneProps with a URL-lookup derivation in Nav.tsx feeding it, plus ~25 call sites), and a mediaBlock() test helper that returned only the first @media match while its sibling declarationsFor() merged all of them — so adding a second block for the same query silently redirected an unrelated CSS contract to the wrong rule.

Test Plan

  • nx test green across website, workspace-react, ui-react, cockpit-shell, cockpit-registry
  • nx e2e website117 passed, 0 failed (four specs encoded the old chrome and were updated to the new behavior; the /docs Run test now clicks the link and asserts the destination returns 200)
  • nx lint website / workspace-react — 0 errors
  • nx build website — succeeds
  • Style contracts added for the new link rules, with a mutation check confirming they fail when the rule is removed (jsdom applies no stylesheets, so these losses are otherwise silent)
  • Browser-verified: exactly one nav[aria-label="Breadcrumb"] per page; callout link computes to the tip tone rgb(26,122,64) at weight 500 while a card link stays undecorated; footer spans the full content width (952px, matching .docs-article-layout, not the 728px article); trail wraps to two lines at 375px without clipping
  • All five CTA destinations return HTTP 200
  • MDX for the six edited files compiles under MdxRenderer's actual remarkGfm/rehypeSlug/rehypePrettyCode pipeline

Notes for review

  • The BreadcrumbList JSON-LD test previously rendered DocsBreadcrumb to prove the structured data matched the visible trail (Google requires the correspondence). Rather than delete that guarantee with the component, it now reads contextTrail off the route's own rendered output — so it can no longer pass by agreeing with a component the route stopped using.
  • The website's trail CSS is scoped under .website-workspace-host deliberately: workspace.css styles the same attributes at the same specificity, so an unscoped rule would win or lose on stylesheet order.
  • The inert prose prose-slate classes on MdxRenderer are left in place on purpose — removing them is a one-line follow-up, but doing it here would make the diff read as a styling migration.

🤖 Generated with Claude Code

blove and others added 17 commits September 3, 2026 06:53
Six changes to /docs: a working Run rail item on the index, search in
place of the Scope card, one breadcrumb trail instead of four, visible
links in docs prose, CTA buttons in the callouts that exist to send
people somewhere, and the search footer on every content page.

Two findings from the running dev server shaped it: Tailwind Typography
is inert in this app, so every docs prose link renders as plain body
text rather than only the callout ones, and /workspace/langgraph/streaming
404s because that capability's canonical destination is its docs route.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nine tasks, 74 TDD steps. Ordered so the data-mdx-chrome opt-out lands
before the callout CTA buttons that need it, and the shell's contextTrail
prop before the route that supplies it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tailwind Typography is inert in this app, so MdxRenderer's prose classes
emitted no rules and --tw-prose-links set a variable nothing read. Every
docs link rendered as plain body text; callouts made it most visible.
Adds explicit .docs-prose link rules, tones callout links to the
callout, marks absolute hrefs as off-site, and gives component anchors a
data-mdx-chrome opt-out.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The docs index is not a capability page, so 'no workspace capability'
left Run a dead control. It now links the canonical example, resolved
through the registry because that capability publishes a docsPath and
/workspace/langgraph/streaming 404s. Passed as a prop so the adapter
comparison page keeps its disabled rail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
One element with href/disabled derived from runHref, instead of two
near-identical branches. ControlPlaneRailItem picks <a> vs <button> from
href truthiness alone, so the accessible role is unchanged in both
directions, and disabledReason is inert while enabled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Task 1 added ~40 lines to docs.css, so the ranges Tasks 3, 5, 6 and 8
cite had shifted. Also notes that .docs-sidebar-search-kbd has TWO rules
(the second overriding font-size/padding), which the deletion range in
Task 3 has to cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Scope card restated the breadcrumb trail. It is replaced by a
full-width search trigger wired to the existing openSearch handoff, the
now-duplicate Search icon leaves the Actions bar, and Actions renders
only when something is left in it. Retires the orphaned
.docs-sidebar-search-* rules the trigger's styling came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The header derived its label from the manifest identity, so a docs route
read 'Ag Ui / Getting Started / Overview' in muted mono. An optional
contextTrail renders a real breadcrumb nav with linked rungs instead.
Absent the prop, cockpit's derived label is unchanged.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
docs-structured-data.spec.tsx renders DocsBreadcrumb to prove the
BreadcrumbList JSON-LD matches the visible trail -- a guarantee Google
requires and the plan's delete-the-component step would have silently
dropped. Re-sources it from the route's own contextTrail, which is
stronger than rendering a component the route may no longer use, and
notes the two stale doc comments that name the component.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CalloutActions/CalloutAction render tone-aware buttons inside a callout,
with target and rel applied automatically for absolute hrefs and
data-mdx-chrome so the new prose underline does not run through them.
Two components rather than a ctaHref prop pair because a callout can
want a primary action and a comparison action side by side. The
authoring rule -- CTAs only in callouts that exist to send you
somewhere -- is documented on the component.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Nav.spec.tsx renders DocsContextContent in the mobile drawer and still
asserted the deleted Scope heading. And a second @media (pointer: coarse)
block exposed mediaBlock() returning only the first match, so the
tooltip contract silently read the wrong block; it now merges every
block for a query, matching declarationsFor's behaviour.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The <p> this replaced carried truncate, so a long label clipped instead
of reflowing the header; the nav needs min-width:0 to yield space in the
flex row the same way. Also pins the invariant that the last rung is
never a link even when it has an href, which was implemented but not
tested.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five callouts across AG-UI and the three runtimes existed only to hand
you a demo link, and read as prose. They now lead with a CTA. Adds the
missing LangGraph one: AG-UI's introduction advertised a live demo and
the flagship adapter's did not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A doc page showed the same trail four times: the shell's derived mono
label, DocsBreadcrumb, DocsPageHeader's lib-dot-section label, and the
Scope card. The shell header now renders an accurate linked trail built
from docs-config; DocsBreadcrumb is deleted and the header keeps only
its library mark and actions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Scope card was its only consumer. It was a required prop on
DocsControlPlaneProps with a URL-lookup derivation in Nav feeding it, so
five call sites kept supplying a value that went nowhere. Also renames
the Nav test whose subject that removal deleted, rather than leaving a
name describing a check it no longer performs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Extracts the index's search prompt into DocsSearchFooter and renders it
below prev/next on every content page. The prompt becomes a real button:
'Press ⌘K' was static text and unactionable on a device with no ⌘K, so
the shortcut is now a hint beside the control rather than the only way in.
The ⌘K pill is aria-hidden, matching the control plane's own search
trigger precedent.

Also hardens the callout CTA buttons (docs.css) with fallbacks on
--callout-tone-text/--callout-tone-surface: those custom properties are
only set on [data-mdx="callout"], so a hypothetical out-of-callout use of
<CalloutActions> would otherwise render an invisible white-on-white
button instead of a visibly wrong one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Four specs encoded behaviour this branch deliberately changed: Run is a
live link on /docs, the page-header lib-dot-section label is gone in
favour of the shell trail, search moved from the pane's bottom Actions
bar to its top, and a new callout shifted the article past a hard-coded
scroll offset. The TOC test now computes its scroll target instead of
using a pixel constant, so the next content edit does not break it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Sep 3, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
threadplane Ready Ready Preview Sep 3, 2026 8:05pm UTC

Request Review

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated approval: this PR received an intelligent (AI) code review. See the review comments on this PR.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

blove and others added 9 commits September 3, 2026 09:10
prose/prose-slate emit zero rules in this app and the four --tw-prose-*
properties were read by nobody. .docs-prose owns docs typography.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
startsWith('http') called //host, mailto: and tel: internal, so they
would have opened in the same tab with no rel. Internal now means
same-document or site-relative, which is correct by construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pill's aria-hidden was load-bearing but untested, so a regression
leaking the shortcut into the button's accessible name stayed green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mediaBlock located blocks by substring, so a compound query like
(pointer: coarse) and (min-width: 600px) would merge into the simpler
one. Matching is now on the complete query text.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
At 375px the four-rung trail wrapped and grew the header. Hides the
Docs and section rungs there, leaving the library link and the current
page. Nothing was clipping; this is spacing, not a fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
It claimed a "working" trigger while only asserting the button
existed. Renamed to describe what it actually verifies (the docs
control plane mounts, search trigger included, on the library-neutral
/docs route); the click-to-dispatch mechanism is already covered by
"closes the drawer before dispatching mobile search" and does not vary
by route, so duplicating it here would add nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The trail already names the library, so a mark in its own row below was
a second statement of it sitting in a nearly empty band. WorkspaceCrumb
gains an optional decorative icon the host supplies, keeping the shell
generic; the page-header row collapses to just the actions, and the two
CSS rules its removed children owned are deleted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
LibraryMark renders a chip with the logo at 60% of it, so size 16 put a
10px logo in a 14px box and read as a smudge. 20 keeps the chip inside
the trail's ~20px line box while giving the logo 12px.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ens"

Measured after the fact: the mobile header is 69px because the nav
trigger is a 44px touch target plus 12px of padding either side. The
trail sits inside that row at 21px, and at 43px when it wrapped it
still fit. So the two-line wrap never grew the header, and the
justification for hiding rungs was simply wrong.

That left the rule costing the Docs back-link and the section rung on
narrow screens in exchange for nothing measurable. Showing all four
rungs is better.

Keeps the DocsPageHeader comment introduced after that commit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove enabled auto-merge (squash) September 3, 2026 20:02
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @blove's task in 0s —— View job


I'll analyze this and get back to you.

@blove
blove merged commit 0aac725 into main Sep 3, 2026
31 checks passed
blove added a commit that referenced this pull request Sep 3, 2026
* docs: design for indexing docs page content in search

Search matches titles, not documentation -- searchableText() only reads
title/description/slug/section/library out of docs-config, never a page
body. That mattered less when search was a power-user shortcut; #986
made it the front door in two places.

Adds a dynamic route that indexes prose at heading granularity and
returns anchor deep links with highlighted snippets, while keeping the
existing client-side title matcher as the instant layer so the fast
path stays fast and a failed request degrades to today's behaviour.

Records two traps found while exploring: outputFileTracingIncludes does
not cover content/docs (api/markdown only works because it is statically
generated), and extract-headings hand-rolls slugification, so the index
must reuse it rather than introduce a second slugger.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: implementation plan for docs content search

Eight tasks, 41 TDD steps. Ordered so the shared tokenizer lands before
the route and query module that import it.

Two refinements from the spec, both recorded in the plan: the wire types
get a dependency-free module (a client component needs DocsSearchHit,
whose natural home transitively imports fs), and DocSection drops the
per-page fields the spec repeated on every record.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(docs): share the search tokenizer with the server

The route needs identical tokenisation, and two copies would drift the
first time a stop word is added.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(docs): pin the search stop-word list

The tokenizer is shared so the client matcher and the server route agree
on what a query means. Adding or removing a stop word changes every
query on both sides identically, so no behavioural test elsewhere would
notice. This is the only thing that catches it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: record why "checkpointer" is the search test fixture

Verified against real content: 16 occurrences in the persistence guide,
7 outside fenced code so it survives the indexer's code stripping, and
it heads two sections so a deep link has somewhere to land. Critically
it is in no page title, which is why today's title-only search cannot
find it -- the test proves the feature rather than passing trivially.

So a failing checkpointer assertion means the pipeline is broken, not
that the term was a bad guess. Do not swap in an easier one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(docs): index doc bodies into per-heading sections

Pure MDX-to-sections indexer: frontmatter and fenced code stripped,
inline code unwrapped, component prose kept. Anchors come from
extract-headings so search deep links cannot drift from the TOC, which
an anchor-parity test pins across all real content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: note the spurious cockpit-retirement failure from apps/website

The plan tells implementers to cd into apps/website for targeted vitest
runs, which makes that spec double-join its WEBSITE_ROOT path and report
3 failures. Verified pre-existing: the file matches origin/main and
nx test website from the repo root is green. Flagged so nobody chases it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(docs): strip markdown syntax from indexed section text

Link targets were searchable, so a query for 'github' or 'docs' matched
every page that happened to link somewhere, and snippets would have
rendered raw [text](url). Keeps link text, unwraps emphasis, and leaves
underscores inside identifiers like TEXT_MESSAGE_CONTENT alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(docs): rank indexed doc sections and build snippets

Weighted AND matching over title, heading and prose, capped at eight to
match the existing result list. Snippets return offsets rather than
HTML so the client renders the marks itself; the window snaps to word
boundaries and overlapping marks are merged rather than emitted raw.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(docs): stop a quoted attribute terminating a component tag

The tag regex scanned to the first > even inside a quoted value, so
title="a > b" leaked the tail of the tag into indexed text. No current
doc trips it; the next one with an arrow in a caption would have, with
nothing to catch it. Also drops the DocSection re-export, which offered
a second import path through the module that reaches fs -- the exact
thing the dependency-free types module exists to prevent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* perf(docs): hoist lowercasing out of the search token loop

Each field was re-lowercased once per query token per section, so a
multi-token query allocated the same strings hundreds of times per
request. Behaviour is unchanged; a mixed-case test now guards against a
half-converted refactor making one field case-sensitive.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(docs): add the docs content search route

Builds the section index once per instance and answers queries from it.
Short queries return empty without scanning, and responses are
cacheable because the corpus only changes on deploy. A per-document
try/catch guards index construction so one malformed doc cannot take
down search for the whole instance.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* build(website): trace docs content for the search route

The route reads MDX at request time and cannot be statically generated
the way api/markdown is, so without this it deploys with no corpus and
returns empty for every query -- silently, and only in production.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: correct Task 5's premise and its verification recipe

Executing it disproved two things I asserted. @vercel/nft already
traces content/docs by statically resolving the fs reads in lib/docs.ts
-- 122 mdx paths in the route trace both before and after the include,
so the include is belt-and-braces rather than the load-bearing fix.

More seriously, the verification I wrote does not verify anything:
next start never consumes .nft.json (only Vercel's builder or an
output:'standalone' build do), and nx build emits to dist/apps/website
rather than the path the recipe used. It would have passed whether or
not tracing worked, which is worse than no check.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: open the search dialog the way the existing e2e does

Task 7 used Meta+k and an unnamed combobox. workspace-shell.spec.ts
clicks the "Search docs" button and addresses the combobox by its
accessible name "Search documentation..." -- more portable across
runners, and the button is the affordance a real user has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* feat(docs): show page-content hits in docs search

Debounced, abortable requests merge server hits beneath the instant
title matches, each showing its section heading and a snippet with the
match highlighted. A failed request falls back to the instant results
rather than surfacing an error.

Title matches and content hits now share one continuous keyboard-
navigable list instead of two disjoint ones: arrow keys and Enter
operate over a combined array so every rendered option is reachable
and selectable, aria-activedescendant and aria-selected stay in sync
with it, and the selected index is clamped whenever the combined list
shrinks (a narrower query, or a slow response landing after arrow
navigation) so it can never dangle past the end. A response for a
query that is no longer current is dropped even when the underlying
fetch ignores the abort signal, so a slow, stale response can never
clobber a newer query's results.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(website): prove prose-only search reaches its section

Searches "checkpointer" — a term that exists only in body prose in the
persistence guide and appears in no page title — and asserts the hit
comes from the content-search group (not the instant title matcher)
and lands on a real section anchor, not just a well-formed fragment.

Adds a negative-control test searching a term confirmed absent from
all docs content, asserting the empty state, so a matcher that always
returns something can't make the positive test pass by accident.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(docs): give the heading scan a single source of truth

The indexer re-implemented extract-headings' fence tracking and heading
regex, then paired the two scans by position. Copy-identical today, but a
change to either would silently misassign anchors -- and the parity test
could not catch it, since the anchors would all still be valid members of
the same file's set. Only the assignment would be wrong, so a deep link
would scroll to the wrong section and look like it worked.

extractHeadings now reports each heading's source line, and indexDocSections
slices the body between those lines instead of re-scanning for fences and
`##`/`###`. There is exactly one place left that decides what a heading is.

Also: clarify the section/sections field collision in IndexedDoc, and strip
un-fenced import lines in toSearchableText so a doc that imports a component
for real JSX use can't leak the import statement into a search snippet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
@blove
blove deleted the blove/docs-page-improvements-66cf88 branch September 3, 2026 22:30
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant