Docs page improvements: one trail, visible links, search-first control plane - #986
Merged
Conversation
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>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
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>
Contributor
5 tasks
blove
enabled auto-merge (squash)
September 3, 2026 20:02
Contributor
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>
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 free
to 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.
Summary
Six improvements to the
/docsexperience, plus two pieces of dead code this work exposed.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/streaming404s: that capability publishes adocsPath, so its canonical destination is/docs/langgraph/guides/streaming?mode=run.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 ademoUrl.Ag Ui / Getting Started / Overview— that casing and "Overview" are manifest facts, not docs facts),DocsBreadcrumb,DocsPageHeader'slib · sectionlabel, and the Scope card.WorkspaceShellgains an optionalcontextTrail; absent it, cockpit's derived label is byte-identical.DocsBreadcrumbis deleted.MdxRenderercarriesprose prose-slateand 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 (measuredrgb(28,28,28), no decoration, weight 400). Explicit.docs-proserules now own it, callout links take the callout's tone, and absolute hrefs get an off-site glyph. Component anchors opt out via a newdata-mdx-chromeattribute.Found and removed along the way: a
pageTitleprop that nothing read (a required field onDocsControlPlanePropswith a URL-lookup derivation inNav.tsxfeeding it, plus ~25 call sites), and amediaBlock()test helper that returned only the first@mediamatch while its siblingdeclarationsFor()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 testgreen acrosswebsite,workspace-react,ui-react,cockpit-shell,cockpit-registrynx e2e website— 117 passed, 0 failed (four specs encoded the old chrome and were updated to the new behavior; the/docsRun test now clicks the link and asserts the destination returns 200)nx lint website/workspace-react— 0 errorsnx build website— succeedsnav[aria-label="Breadcrumb"]per page; callout link computes to the tip tonergb(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 clippingMdxRenderer's actualremarkGfm/rehypeSlug/rehypePrettyCodepipelineNotes for review
DocsBreadcrumbto prove the structured data matched the visible trail (Google requires the correspondence). Rather than delete that guarantee with the component, it now readscontextTrailoff the route's own rendered output — so it can no longer pass by agreeing with a component the route stopped using..website-workspace-hostdeliberately:workspace.cssstyles the same attributes at the same specificity, so an unscoped rule would win or lose on stylesheet order.prose prose-slateclasses onMdxRendererare 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