Skip to content

Updates the Docs home page - #3866

Draft
yetanothertw wants to merge 13 commits into
mainfrom
vectordb-docs-home
Draft

Updates the Docs home page#3866
yetanothertw wants to merge 13 commits into
mainfrom
vectordb-docs-home

Conversation

@yetanothertw

Copy link
Copy Markdown
Member

Fixes #1533 and splits the Solutions and use cases section into 4 tiles.

Image

@Mpdreamz Mpdreamz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Just 'View docs' ?

We don't need to bleed our complexity here IMO :)

@yetanothertw

yetanothertw commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

Just 'View docs' ?

We don't need to bleed our complexity here IMO :)

Yes, this is up for review, so might definitely change. It'll be the odd one out regardless of what the copy says 😅

I've updated it, so here's what it looks like now:

image

Mpdreamz and others added 9 commits August 14, 2026 12:26
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Add hub page layout and the {hero} directive

Adds the first two pieces of the hub page mechanism, rebuilt on the current
navigation implementation. The prototype branch is merged with nav-v2, which is
out of scope, so nothing from nav-v2 ports.

Adds `layout: hub`, which drops the right-rail table of contents and the
previous/next navigation, keeps the left sidebar, and gives the body the full
content column. Page-title detection gains two fallbacks, so a page composed
only of directives still resolves a title: an H1 nested in a directive, then the
first {hero} directive's :title: option.

Adds {hero}, the identity band. It carries a product icon, the page title, a
description, and up to three optional actions.

Promotes the prototype's hub-only link validator to a shared
DirectiveLinkValidator. Directive links never pass through Markdig's inline link
parser, so every directive that takes a URL had invented its own handling.
{page-card} moves onto the shared class and its hand-rolled copy goes. It keeps
its previous contract: relative links resolve against the source file, and no
file-existence check runs, because page-card links can target generated pages
that have no markdown file on disk.

Guards DirectiveBlockParser.TryContinue so an ancestor directive stops consuming
option lines once it has opened a nested directive child. Without the guard the
ancestor swallows every descendant's options and the last one wins. This affects
every nested directive, so it lands with regression tests over tab sets,
steppers, and dropdowns.

Adds LLM markdown and plain-text renderers for {hero}. A hub page has no body
prose, so without them it exports as an empty shell. The plain-text renderer
feeds the search index and emits the hero title and description only. Hub pages
answer generic "<product> docs" queries. Indexing section and card titles would
let a hub compete with the pages it links to on specific queries.

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

* Fix hub page regressions found in the rendered output

Three defects, all introduced in this branch rather than carried from the
prototype.

Suppress the page-level H1 and the applies-to badge on a hub page. The prototype
guards both in Index.cshtml and that guard was missed, so every hub page rendered
the page title twice: once from Index.cshtml, once from {hero}. This also
delivers the applies-to suppression that #1386 records but the code did not do.

Drop the markdown-content class from the hub article. The class was added to
reach the JS hooks, but those select on the markdown-content id, not the class.
typography.css is unlayered and hub.css sits in @layer components, so unlayered
rules win regardless of specificity. The class made typography.css restyle the
hero eyebrow as a blue underlined link and skew the icon alignment against the
heading. The id stays, so hljs, smooth-scroll and toc-nav keep working.

Give hero actions the same link treatment as inline links. An external action now
opens in a new tab with rel="noopener noreferrer" and no preload, and an anchor
action no longer preloads. Only an internal link preloads.

Fix an empty href for "/index.md". Stripping the whole "/index.md" segment left
an empty string rather than the site root.

The example page gains three actions, one internal and two external, to
demonstrate both forms.

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

* Align the hero product mark with the text column

The mark sat about 9px right of the eyebrow and the description below it. Two
insets stacked. Each icon's viewBox carried transparent padding around the glyph,
which cost 5px at the rendered size. The 48px chip then centred the 40px SVG
inside itself, which cost another 4px.

Crops every icon's viewBox to its glyph, so no transparent padding is left to
absorb, and lets the box hug the mark instead of forcing it into a fixed square.
Width now follows from the glyph's own aspect ratio.

The letter fallback keeps the chip. It has no glyph to align, and the chip's own
edge already lines up with the text column.

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

* Make the example a docs-builder hub, and keep cross-links out of new tabs

The example page was an Elasticsearch hub whose links had all been repointed at
docs-builder pages, leaving an Elasticsearch name, icon, hero and headings on top
of docs-builder content. It is now a docs-builder hub throughout, renamed to
docs/examples/products/docs-builder.md.

Its hero demonstrates all three link forms: a site-absolute path, a docs-content
cross-link, and an in-page anchor. The unknown `docs-builder` icon key also
exercises the single-letter fallback.

Fixes a defect the cross-link uncovered. A cross-link resolves to a full URL, so
the hero treated it as external and opened it in a new tab. Inline links make the
opposite call, and deliberately so: a cross-link still points at documentation
this site serves. DirectiveLinkValidator now records the URLs it resolved from a
cross-link scheme, and the view model consults that rather than guessing from the
scheme.

Adds doesNotContainHtml to the authoring assertions.

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

* Apply review feedback to the hero and hub layout

Martijn's review on #3825 and theletterf's questions.

- The hero no longer adds its own horizontal padding. The content column
  supplies the gutter, so the hero aligned with the sections below it only
  above the shared max-width. Below it the hero sat 24px further in.
- Hero type moves onto the site's scale. The root font size is 14px, so
  values written as 16px-root arithmetic rendered around 12% small. The
  description in particular used a size found nowhere else on the site.
- The three hero actions carry equal weight and take the secondary button
  treatment from the {button} directive, rather than a bespoke hero button.
  The option names set order, not weight.
- A page with `layout: hub` and no {hero} now fails the build. The layout
  removes the page H1, so without a hero the page has no title at all and
  falls back to its file path.

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

* Raise the hero eyebrow to body size

Martijn's point about tiny fonts applies to every piece of hub text, not
only the ones in his screenshot. The eyebrow is a reading affordance, so it
takes the body size.

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

* Follow the site's rule for arrows on links

The site puts an arrow on two kinds of link: one that sends the reader
onward out of a section, such as "View all reference docs" on the landing
page, and a row that is itself the target, such as {page-card} or the
previous and next footer. It marks no ordinary link and no button.

The hero followed neither. Its actions are buttons, so they carried an
arrow the site gives no button, and only when the target was an in-page
anchor. Both are gone.

The eyebrow keeps its arrow, because it sends the reader onward to the docs
home. It now uses the site's glyph and hover slide through one shared
`.hub-arrow` class, rather than a second arrow drawn by hand.

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

* Put the hero actions on the neutral button variant

#3853 added `:type: neutral` to the {button} directive, a monochrome
outline for clusters where blue competes with the primary action. That is
exactly what the hero needs: three actions of equal rank, none of which
should claim to be the call to action.

#3853 also flagged that this stack used border colours failing the 3:1
non-text contrast ratio. The eyebrow pill moves from grey-20, which reads
at 1.32:1, to grey-70 at 3.31:1. The border is what identifies that
control, so it has to clear the threshold.

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

* Add Logstash icon to hub ProductIcons (#3843)

Hub heroes fall back to initials when a product key is missing from the
inline SVG map. Logstash hubs need the product mark.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Brandon Morelli <brandon.morelli@elastic.co>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Martijn Laarman <Mpdreamz@gmail.com>
* Add {card-group}, {link-card}, and {explore}

These three ship together because {explore} is what switches the other two into
their second rendering mode. A reviewer cannot judge the mechanism from any one
of them alone.

{card-group} is a section heading and a card grid. {link-card} is one card, with
a title, a description, a primary link list, and an optional aside. Its body is a
fixed YAML schema rather than markdown, so a missing title or invalid YAML fails
the build. {explore} wraps card groups into a stack of collapsible accordions.

Nesting drives the mode switch, not an option. Inside {explore} a card group
renders as an accordion and a link card renders as a titled link column, with the
description dropped and the aside rendered as a badge cluster under its own
authored label. Driving this from the ancestor rather than a per-directive option
means every card grid elsewhere on the site keeps working unchanged.

Adds HubDirectiveViewModel, which centralises the attributes a hub link needs.
Hub links come from options and YAML bodies, so they never pass through Markdig's
link renderer and each view was repeating the external, anchor, and preload rules
by hand. {hero} moves onto it too.

The LLM export keeps the whole structure. The curated grouping of links is what a
hub page is for, and the nav tree in llms.txt is neither ordered nor curated, so
it is not a substitute. The search body deliberately gets nothing from these three
directives: section, card, and link titles are the tokens that would let a hub
outrank the pages it links to on a specific query.

Adds doesNotContainHtml to the authoring assertions, for content a directive
drops on purpose.

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

* Apply review feedback to cards and the explore section

Martijn's review on #3826.

- Card link lists and column link lists take the site's standard link
  affordance: a bullet, blue, underlined. The leading chevron is gone. An
  arrow now means a control that behaves like a button, such as a hero
  action, rather than an entry in a link index.
- The aside cluster takes the {button} directive's secondary treatment in a
  new small size, because those are buttons rather than cards. The size
  modifier composes with either button type and is available to any caller.
- Only a card that carries its own link lifts on hover, and that card now
  has a whole-card target. A card holding several links and no link of its
  own no longer promises a click it cannot answer.
- Card and explore type moves onto the site's scale.
- Explore accordions no longer share a `name`, so a reader can hold several
  open and compare them.

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

* Raise card and explore text to body size

Martijn's point about tiny fonts applies to every piece of hub text. Card
descriptions, column descriptions, and aside links now read at the body
size. The two cluster labels sit one step down, so they stay distinct from
the column headings beside them, and nothing renders below 14px.

The aside buttons keep 14px. They support the column links above them, so
they must not outweigh them.

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

* Drop the now unused anchor flag on a hero action

The flag only ever drove the arrow, and the shared link attributes do their
own anchor check for preloading.

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

* Drop the aside from a link card

A link card held two kinds of link: a primary list and an aside that
rendered as a badge cluster in a column, or as inline dot-separated links in
a card. That is two visual languages for the same thing, in one card.

The aside also could not express what it promised. It said "these are
secondary", and the only signal was its label. At 12px that label was too
small to read, and at 14px it was indistinguishable from the column heading
beside it. There is no size that reads as subordinate but legible.

A second group of links now becomes a second card. One shape, one
treatment, everywhere.

This removes the `aside` field, both of its renderings, the badge styling,
and the small button size added for it, which nothing else used. It also
removes the label sizing question and the sub-24px target on the badges.

Follow-ups: elastic/docs-content-internal#1385 and #1549 both specify
`aside` and need updating.

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

* Make the linked card's hover state visible

The hover border moved from grey-20 to grey-30, a change from 1.22:1 to
1.49:1 against white. That is not a visible state change, and a state
indicator has to clear 3:1. It now uses grey-80 at 4.15:1, which is what
{page-card} already uses for the same job.

The resting border stays grey-20. It is a container edge rather than the
thing that identifies the control, and {page-card} rests there too.

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

* Restore the explore container width

The explore stack lost its max-width, so the accordion rows ran the full
content column while the section heading above them stayed within the
shared width. The rows started well left of their own heading.

The rule was collateral damage when the aside styling was removed: the
deleted range ran past the aside block and took the container with it.

The accordion also drops its horizontal padding, so a title starts on the
same line as the section heading and the toggle icon ends on the same line
as its right edge.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add {get-started}

The onboarding section of a hub page. It gives a new reader one opinionated path
to a first success, before they face the full link list. The section is optional.

Keeps the top-level install and tutorial pair as first-class fields rather than
treating it as prototype back-compatibility. The designs use exactly that shape:
a copyable command beside a tutorial card, with numbered steps below.

The install snippet renders inside `.highlight pre` with a language class, which
is what the site's copy button and syntax highlighter already select on. Both
work with no extra wiring, because the hub layout kept the markdown-content id.

Steps take one of three shapes: plain, a whole-card link, or two or more equally
weighted options side by side. Numbering is automatic and sits before the title,
because the section describes a sequence and the number is what carries that.

Drops the prototype's per-step icon field. The icons restate the step title, the
number badge is already the visual anchor, and a schema field for decoration
makes every author pick icons that then drift between hubs.

The LLM export renders the steps as an ordered list with options as sub-items.
The search body gets nothing, for the same reason as the other hub directives.

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

* Apply review feedback to the get-started section

Martijn's review on #3825 covered the whole page, so the same rules apply
here.

- Get started type moves onto the site's scale. The values it carried were
  16px-root arithmetic against a 14px root, so every one rendered small.
- Step numbers move from grey-70 to ink-light. grey-70 reads at 3.31:1 on
  white, short of the 4.5:1 minimum, and ink-light carries the same quiet
  weight at 7.03:1.

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

* Raise get-started text to body size

Martijn's point about tiny fonts applies to every piece of hub text. Step
descriptions, option labels, option descriptions, option buttons, and the
step links now read at the body size. The install command keeps 14px, which
is what every other code block on the site renders at.

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

* Use the shared arrow in get-started

Both links that carry an arrow here send the reader onward out of the card,
which is one of the two cases the site marks. They now use the same glyph
and hover slide as every other hub arrow. The option button carries none,
like every other button on the site.

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

* Let a get-started section run to any number of steps

Nothing in the schema limited the section to three steps. One CSS rule did:
when a step carried options, the grid locked to two tracks, which suits one
wide step and two others exactly. A fourth step left one card alone on a
half-width row.

The directive now counts the steps that flow in columns and sets the track
count. Three across when they divide by three, two when they are even, so
the last row is never short. A step carrying options still spans the full
row and takes no track. Below 640px of available width the steps stack.

The width is measured on the section rather than the viewport, because the
left sidebar narrows the body.

The example hub gains a fourth step, so the preview shows the new
arrangement.

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

* Give a linked step the shared card hover

The step card hovered to a blue border while the other two card types
hovered to grey. Blue also makes one step in a sequence look like the call
to action. It now uses grey-80, the same treatment as {page-card} and a
linked {link-card}.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add {whats-new}

A recency panel for a hub page. A reader who bookmarks a hub wants a quick answer
to "what changed recently" without hunting through release notes.

The panel content moves out of the build tool. The prototype reads
config/whats-new.yml from docs-builder, which means a writer editing a highlight
card opens a pull request against the build tool and waits for a release. It now
reads hub-whats-new.yml from the root of the current documentation set, beside
changelog.yml and redirects.yml.

That makes the directive documentation-set scoped. It cannot render another
repository's panel, and this is a constraint rather than a syntax gap.
Cross-link resolution maps pages through the link index, and a YAML data file is
not a page. In an isolated build the other repository is not checked out, so
there would be no file to read. Every hub page lives in the same repository as
its content file, so the limit costs nothing today.

The common case stays one line, `:product: <key>`. Omitting it accepts the same
schema as an inline YAML body, for a one-off panel.

Adds containsRawHtml to the authoring assertions. The pretty-printed comparison
only matches a childless element or a snapshot taken from the outermost element
of the directive output, so asserting a class on a container previously meant
snapshotting its whole subtree.

Follow-up, not in this change: a skill in docs-content that updates
hub-whats-new.yml from the release notes of each product that has a hub page.

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

* Apply review feedback to the what's new section

Martijn's review on #3825 covered the whole page, so the same rule applies
here. What's new type moves onto the site's scale. Every raw font size in
hub.css is now a scale token.

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

* Raise what's new text to body size, and stop shouting the date

Martijn's point about tiny fonts applies to every piece of hub text. Card
descriptions, release note links, and the footer now read at the body size.
Card meta sits one step down at 14px. No hub text renders below 14px, and
nothing uses the smallest step of the scale.

The card date no longer forces uppercase. It is authored, so it keeps the
casing the writer chose. The example data and the syntax page now use
sentence case to match.

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

* Use the shared arrow in what's new

The card link and the upgrade link both send the reader onward, which is one
of the two cases the site marks with an arrow. They now use the same glyph
and hover slide as every other hub arrow. No hub directive draws its own
arrow any more.

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

* Give a what's new card the shared card hover

The card hovered to a pale blue border reading 2.12:1 against white, so the
state was barely visible. It now uses grey-80 at 4.15:1, the same treatment
as every other card on a hub page that is itself a link.

Every card that is a link now hovers the same way, and every state border
in the hub clears the 3:1 ratio.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* Add legacy /guide AsciiDoc-to-Markdown migration tool

Implements a native C# converter to migrate elastic.co/guide content
(AsciiDoc served by the legacy elastic/docs system) into Markdown
docsets that docs-builder can render.

New project: Elastic.LegacyDocs.Migration with:
- conf.yaml parser for the elastic/docs book definitions
- Source repo manager for resolving branches and paths
- AsciiDoc lexer, parser, and AST (sections, code blocks, tables,
  admonitions, lists, includes, conditionals, inline formatting)
- Markdown emitter targeting MyST syntax
- Page chunker to split documents by section level
- Archive and Latest docset generators producing docset.yml/toc.yml
- CLI command: `docs-builder guide migrate`

Made-with: Cursor

* Fix migration output to pass docs-builder validation

Fixes found by running the converter end-to-end and building with
docs-builder:

- Fix section level off-by-one in lexer (= is level 0, == is level 1)
- Fix Repos model to handle nested YAML objects (Dict<string, LegacyRepo>)
- Fix basePath in generators to use index file directory for includes
- Fix chunker to walk into level-0 sections and promote chunked
  sections to H1 for standalone pages
- Use docs-builder anchor syntax (# Title [anchor]) instead of MyST
  (anchor)= targets
- Map unsupported directives: caution→warning, sidebar→admonition
- Add cross-page anchor-to-slug map so refs emit file links
- Add bare URL[text] pattern to inline parser
- Prevent double image path prefix

Made-with: Cursor

* Extract migration to isolated CLI tool and add missing parser features

- Add DSV/CSV/TSV table format support (384 occurrences in ES|QL docs)
- Add pass:[] and +inline+ passthrough inline patterns
- Add unconstrained bold (**text**) support
- Add [role]#text# inline macro support
- Extract migration command into standalone docs-migrate CLI tool
  (removable by deleting src/tooling/docs-migrate/ and
  src/authoring/Elastic.LegacyDocs.Migration/)
- Remove migration coupling from docs-builder

Made-with: Cursor

* Refactor docs-migrate into subcommands and switch to bare-clone + worktree strategy

- Split monolithic MigrateCommand into init/list/clone/convert/serve subcommands
- SourceRepoManager now uses bare clones + git worktrees + sparse checkout (cone mode)
  to avoid redundant full clones per branch and only fetch needed directory trees
- Add Proc package for process execution in place of raw System.Diagnostics.Process
- Add guide-nav feature flag to configuration layer (FeatureFlags, DocumentationSetFile,
  ConfigurationFile) so converted docsets can signal the guide-archive nav mode

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix conf.yaml parser missing nested section groups

The conf.yaml structure has up to 3 levels of nesting: a category's
sections can contain sub-groups (with base_dir + sections) rather than
books directly. LegacyBook had no Sections property so IgnoreUnmatchedProperties
silently dropped all nested books, causing entire families to disappear
(Elasticsearch clients, all APM agents, ECS logging, etc.).

Add BaseDir and Sections to LegacyBook, then recursively flatten after
deserialization, accumulating the base_dir prefix so leaf book prefixes
are fully qualified (e.g. en/elasticsearch/client/net-api).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Normalize docs-migrate default work dir to AppData

Use LocalApplicationData/elastic/docs-migrate as the default work
directory, matching the pattern from Paths.ApplicationData in the main
codebase. Includes the same Docker/CI fallback to GetTempPath() when
LocalApplicationData returns an empty string.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Write conversion output to cwd/.artifacts/migrated

Output from convert and serve now lands in .artifacts/migrated relative
to the current working directory, separate from the AppData work dir
that holds conf.yaml and repo clones.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Add --no-hud flag to serve and fix branch resolution for repos without versioned branches

- Add --no-hud to docs-builder serve: disables diagnostics HUD and
  skips background in-memory validation builds
- docs-migrate serve passes --no-hud automatically
- Fix SourceRepoManager branch resolution: check local branches before
  fetching, use --detach for worktrees so repos like elastic/docs
  (only has master) can serve multiple version worktrees
- Use Proc.Start for branch probing to suppress expected git errors

Made-with: Cursor

* Rewrite guide URLs to internal links and emit docs-builder substitutions

- Rewrite https://www.elastic.co/guide/... URLs to absolute internal
  markdown links (/prefix/version/page.md) so docs-builder validates them
- Emit {{name}} (double braces) for unresolved AsciiDoc attribute
  references so docs-builder recognizes them as substitution variables
- Add SharedAttributes with ~130 well-known product-name attributes
  from shared/attributes.asciidoc (es, kib, agent, fleet, etc.)
- Write subs: section in generated docset.yml with all product names

Made-with: Cursor

* Preserve block anchors as docs-builder inline anchors

AsciiDoc [[id]] anchors on non-section blocks (paragraphs, tables,
description lists, etc.) were silently discarded. Now they are wrapped
in an AnchoredBlock node and emitted as $$$id$$$ inline anchors that
docs-builder renders as <a id="..."></a> link targets.

Made-with: Cursor

* Fix conditionals inside verbatim blocks, anchor map coverage, and source attributes

- Fix lexer: conditionals inside verbatim/table blocks were being parsed
  as conditional tokens instead of raw text; now only recognized outside
  those contexts
- Fix table delimiter regex to accept |=== with 3+ = signs
- Fix parser: attribute substitution in section titles and image alt/title;
  preserve pending id/title/blockAttr across list-continuation blocks;
  track pendingStart to reset pos on section-level break
- Fix page chunker: include AnchoredBlock IDs in anchor-to-slug map
- Fix ConvertCommand: pass source_branch and per-repo root attributes to
  parser; guard against missing Current version when building overview

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Migrate docs-migrate CLI to Nullean.Argh

docs-migrate was the last CLI still on ConsoleAppFramework, which had no
PackageVersion entry and so failed restore under Central Package Management.
Align it with docs-builder and essc: Host.CreateApplicationBuilder +
AddArgh, app.Map<T>() registration, and CancellationToken in place of Cancel.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix converter defects blocking elastic.co/guide book migration

Fixes eight root-cause bugs that produced garbled output across 21,962+
files in the converted corpus. All changes are covered by a new
tests/Elastic.LegacyDocs.Migration.Tests/ project (19 tests).

Step 0: Add test project, diagnostics channel (OnDiagnostic on
  AsciidocParserOptions), and AGENTS.md test-project table row.

Step 1: Fix `--` open blocks — lexer dispatched on delimChar before
  checking length so `--` fell into the verbatim path. Now length < 4
  routes to BlockDelimiter; IsMatchingDelimiter/IsMatchingClose require
  exact length so `--------` can no longer close a `----` block.
  PageChunker now recurses into OpenBlockNode for anchor collection and
  page extraction.

Step 2: Add IncludeDirective case to ParseBlock's switch — includes
  were silently skipped (SkipToken) instead of being processed.

Step 3: Attribute resolution — eager expansion via SetAttribute helper,
  SetBasePath keeps docdir in sync, Path.GetFullPath normalization in
  ProcessInclude fixes 863 `../../shared/attributes.asciidoc` paths,
  docs-root/asciidoc-dir seeded in ConvertCommand, docdir saved and
  restored across include file boundaries.

Step 4: Emitter substitution — undefined attributes emit `{name}` not
  `{{name}}`; ProductNames keys pass through as `{{name}}` for
  docs-builder subs. SetAttribute skips ProductNames so shared/
  attributes.asciidoc cannot shadow them.

Step 5: Callout annotations — populate CodeBlockNode.Callouts from
  trailing `<n> text` lines after a code block closing delimiter,
  making the emitter's ordered-list output live.

Step 6: include-tagged:: — new ResolveVerbatimIncludes handles
  `include-tagged::` directives inside listing blocks; tag/end regexes
  widened to allow hyphens and drop the //prefix requirement.

Step 7: Xref regex uses non-greedy `.+?` to allow `>` in link text;
  multi-line admonition paragraphs collect continuation Text tokens
  instead of truncating at the first line.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix 4 conversion quality bugs in MarkdownEmitter

- Headings: apply attribute substitution to document/section titles so
  {es} → {{es}} rather than leaking as raw text
- Same-page xrefs: compare resolved anchor slug against current page
  slug; emit #anchor for same-page and slug.md#anchor for cross-page
- AnchoredBlock: emit a blank line after $$$anchor$$$ so docs-builder
  treats it as a standalone inline anchor block
- titleabbrev: PassthroughNode wrapping <titleabbrev>…</titleabbrev>
  was missed by the ParagraphNode filter; add the check to the
  PassthroughNode emit branch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs-migrate: --minors flag, default port 3001, version URL routing fix

- Add --minors N to clone and convert commands to cap minor versions
  per major; stored in FilterOptions and persisted to .clone-options.json
- Change docs-migrate serve default port from 3000 to 3001
- Fix /8.19 URL routing: Path.GetExtension treats version segments like
  8.19 as having extension .19; restrict hasKnownExtension check to
  actual document file types so version-number slugs resolve correctly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix xref display text, link attr substitution, and chunk level

- Build AnchorToTitleMap alongside AnchorToSlugMap in PageChunker;
  when a section has <titleabbrev>, use that as the display title
  (matches the abbreviated nav title on elastic.co, e.g. "Indices and
  documents" vs full "Indices, documents, and fields")
- EmitCrossRef: resolve <<anchor>> with no text to the section title
  from AnchorToTitleMap, not the raw anchor ID
- EmitLink: apply SubstituteTitleAttrs to link text so {es}//{kib}
  references in URL link text become {{es}}/{{kib}} instead of leaking
  as single-brace literals
- ConvertCommand: conf.yaml chunk:N maps to chunkLevel N+1 in our AST
  (chunk:1 → chunkLevel 2 creates separate pages for === sections,
  matching elastic.co's page-per-section structure; 423 → 3376 pages)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix xref conversion: inline regex ordering, bold/italic spanning, DL multi-line collection, title xrefs

- Prevent double-backtick AsciiDoc curly-quotes from opening a code span by adding
  (?<!`) lookbehind on the mono pattern (group 16)
- Restrict bold (groups 13,14) and italic (group 15) from matching across < so they
  cannot consume <<anchor>> xref openers in cross-mark content (e.g. *word*<<xref>>)
- Fix ParseDescriptionList to collect consecutive Text tokens into one paragraph
  (matching ParseParagraph behaviour) so xrefs that wrap across continuation lines
  within a DL item are kept in a single ParseInlines call
- Add SubstituteTitleXrefs helper to MarkdownEmitter and apply it to section headings,
  document titles, and callout descriptions, which previously used SubstituteTitleAttrs
  only and left <<anchor,text>> patterns unconverted
- Add ifeval:: lexer fix: relax middle group from .+ to .* to allow empty attribute name
- Add GetBlockAttributeRegex trailing-whitespace trim to avoid false negatives
- Add test coverage: curly-quote context, ordered-list items, bold-asterisk interference

Result: raw xref leftovers in en/elasticsearch/reference/8.19 drop from 273 to 9 files.
The remaining 9 are source-level bugs (malformed single >), code-block placeholders,
or xrefs genuinely inside code spans -- all correct not-converted behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Inline include:: directives inside pipe tables

When a pipe table contains an include:: on its own line (typical
for shared common-options snippets), the lexer emits it as a Text
token (not IncludeDirective) because the inTable state flag is not
set during include resolution. ParseTable now detects the include::
pattern in Text tokens, reads the referenced file, and appends its
| ... | rows directly to the current table instead of leaking the
directive text into the last cell.

Also handles the IncludeDirective token type for future-proofing.

Result: include:: leftovers drop from 58 to 24 (remaining 24 are
{esql-specs} references to external test fixture files that are not
part of the repository and cannot be resolved).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix callout collection: skip comments/blank after code blocks, add ParseCalloutList for orphan markers

- After a listing block, skip any trailing comment tokens (// TEST[...]) and
  one optional blank line before attempting callout collection; restore position
  if no callout markers follow, preventing skipped blank lines from orphaning later content
- Add ParseCalloutList routed from ParseBlock for standalone <n> callout lines
  that appear after continuation text or in other contexts where the code-block
  collector already broke; collects multi-line descriptions via continuation join
- Lexer: relax ifeval:: middle group .+ → .* to allow empty attribute name
- Lexer: allow trailing whitespace on block attribute lines [attr]\s*

Result: raw <n> callout markers in en/elasticsearch/reference/8.19 drop from ~396 to ~90 files.
Remaining cases are multi-callout blocks where <1> description wraps across lines,
breaking the code-block collector before reaching <2>+.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix multi-line callout descriptions and DL inline xref spanning

- ParseParagraph: stop collecting Text tokens at callout markers (<n>)
  so ParseCalloutList gets to handle the run instead of having them swallowed
  into paragraph content
- ParseDescriptionList: same guard in the inline Text-continuation loop
- Code-block callout collector: collect continuation lines for each <n>
  description so multi-line callout text is fully captured rather than
  truncated at the first line
- ParseDescriptionList: when inline description text (after ::) contains
  an unclosed <<, join continuation Text tokens until the xref closes,
  enabling multi-line xref spanning within DL items

Result: callout markers drop from 90 to 0; residual raw xrefs drop from 23
to 22 all of which are either inside code spans/fences (correct behaviour)
or source-level malformed >> (not fixable).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Load shared/attributes.asciidoc before converting each book

Pre-parse docs-repo/shared/attributes.asciidoc using the existing AsciidocParser
with branch/version seed attributes already set, so feature and product-name
abbreviations like {transform}, {ilm-init}, {anomaly-detect} and cross-guide
URL attributes like {ref}, {logstash-ref} are resolved at conversion time.

ProductNames keys (es, kib, etc.) are still excluded from eager expansion so they
continue to emit as {{name}} docs-builder substitution placeholders that get
resolved from the generated docset.yml subs: block.

Adds AsciidocParser.LoadAttributeFile static helper and ResolvedAttributes
property to support this pre-parse pattern.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Convert AsciiDoc typographic double quotes to straight quotes

Add group 23 to InlineCombinedRegex to match ``text'' (AsciiDoc opening
double-backtick + closing double-apostrophe) and emit as "text" (straight
double-quote pair). The existing lookbehind on the mono group already
prevents `` from opening a code span, so group 23 matches the remaining
cases that fall through to raw text. Eliminates 21 occurrences of visible
AsciiDoc typographic quote syntax in en/elasticsearch/reference/8.19.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix chunking: each included file becomes a separate page regardless of section level

- Add IsIncludeRoot flag to SectionNode to mark sections produced at the
  top level of a ProcessInclude call
- Set IsIncludeRoot=true in ParseSection's IncludeDirective handler and in
  ProcessInclude's main loop
- Fix chunkLevel calculation: conf.yaml chunk:N maps directly to AST level N,
  so pass book.Chunk directly instead of book.Chunk+1
- Fix ExtractPages to recurse into inline sections (level > chunkLevel) when
  looking for nested IsIncludeRoot sections — this handles the case where an
  include appears after a discrete section, which nests the included content
  under the discrete section in the AST
- Fix CollectAnchors to also recurse into inline sections for the same reason
- Add IncludeChain_EachIncludedFile_BecomesASeparatePage test that mirrors
  the elastic.co search-your-data structure with 3-level include nesting and
  discrete sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix: emit $$$anchor$$$ before H1 headings so docs-builder registers them

docs-builder's anchor registry includes H2+ heading slugs and $$$...$$$ inline
anchors but NOT H1 heading slugs. Cross-references like
[text](page.md#page-anchor-id) were triggering 96 'does not exist' errors
because the page-level anchor (from the original [[block-anchor]] before the
first section) was emitted as '# Title [#anchor-id]' — a format docs-builder
parses for H2+ but does not register in the anchor collection for H1.

Fix: emit $$$anchor-id$$$ on a standalone line before H1 headings (both from
document.Title and from Level=0 SectionNode). The $$$...$$$ syntax is the
docs-builder InlineAnchor format, which is collected via
document.Descendants<InlineAnchor>() and included in markdown.Anchors.

With this fix: 0 errors and 0 warnings from docs-builder serve.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix verbatim include resolution, ifeval stripping, and +..+ passthrough bug

- Extend ResolveVerbatimIncludes to handle standard include::[tag=] and
  full-file include::[] directives inside verbatim blocks (fixes 164 ESQL pages)
- Strip ifeval::/ifdef::/endif:: conditional markers from verbatim block
  content where they can't be evaluated (fixes 2 files)
- Add lookbehind to constrained +..+ passthrough regex so '8.0+' and similar
  version indicators don't start a passthrough span (fixes xref leaking past
  the passthrough into prose)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LF2iCfVK3ecJgPeFCP6p6S

* ServeCommand: catch OperationCanceledException so Ctrl+C exits cleanly

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Fix ifeval conditionals, trailing-space delimiters, and double-xref normalization

- ConditionalProcessor: for ifeval::[expr], the condition expression lives
  inside the brackets (token.Metadata.Content / group 3 of the lexer regex),
  not before them (Condition / group 2 = empty). Read Content as the
  condition for ifeval and always push to conditionStack (ifeval is always
  block-level, never inline). Reduces leaked ifeval/endif markers from 126
  files down to 3 in the logstash-versioned-plugins corpus.

- AsciidocLexer.IsMatchingDelimiter + AsciidocParser.IsMatchingClose: both
  now trim trailing whitespace before length/char comparison so a source line
  like '---- ' (trailing space) correctly closes a '----' verbatim block.
  Fixes ~20 pages in app-search and fleet books where headings and xrefs
  appeared raw inside unclosed code fences.

- AsciidocParser.ParseInlines: pre-normalize '<<<<target>>, text>>'
  (double '<<' source authoring bug) to '<<target, text>>' before regex
  matching, fixing the malformed '[<<anchor](#<<anchor)' link in the
  app-search crawl-proxy page.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix: trim openingDelim in parser + route [sidebar] blocks correctly

Two related fixes in AsciidocParser:

1. `openingDelim = token.Raw.TrimEnd()` — the parser was keeping
   trailing whitespace from the raw delimiter line (e.g. `--  ` or
   `-----  `), so IsMatchingClose's exact-length check would fail and
   the block never closed. Trimming at assignment fixes all downstream
   comparisons in both the verbatim and non-verbatim paths.

2. `[sidebar]` style on a `--` open block now routes to SidebarNode
   instead of OpenBlockNode, matching docs-builder's directive.

Previously these caused entire sections (including subsequent headings
and source blocks) to be consumed as unclosed block content, appearing
raw in the output.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Fix: dotnet format whitespace in test files

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Fix table comment leak, long-dash code fence tests, and book filter persistence

- Lexer inTable branch: emit Comment tokens for // lines (tag::, end:: markers)
  so they are silently skipped rather than appended to the last table cell.
- Tests: two EmitterTests covering 50-dash code block delimiters used in
  Watcher/Shield 2.4 docs (valid AsciiDoc: 4+ dashes).
- SharedOptions: don't fall back to saved Book when --book is not passed;
  a bare `convert` should convert all books, not repeat the last --book filter.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Regenerate CLI schema and fix Prettier formatting

- docs/cli-schema.json: updated to include --no-hud flag added in ServeCommand
- Frontend TSX/TS files: auto-fixed by Prettier (18 files from merge)

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* fix: don't persist book filter across docs-migrate runs (#3835)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>

* Add adoc-compare dev tool and register it in the solution

Small standalone CLI that parses a single .adoc file and prints the
emitted Markdown to stdout — useful for local spot-checking the
parser/emitter without running the full docs-migrate pipeline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: add required license header to adoc-compare Program.cs

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
…ectionsByLevel, HeadingLevelBase (#3833)

- PageChunker: tree-shaped PageOutput with child pages, slug dedup, IsDiscrete support, AutoId replacing Slugify.Core
- AsciidocParser: IsDiscrete flag for [discrete]/[float] sections; NestSectionsByLevel post-processes flat include results
- MarkdownEmitter: HeadingLevelBase rebases heading levels per page root; LF-only newlines
- YamlWriter: Island and Children on TocEntry; nested toc entries with island: true
- ConvertCommand: Island=true per version toc entry; WritePages recursion into child pages; navigation_title frontmatter; island: true per book in docset.yml
- FeatureFlags: GuideNavEnabled property
- Paths/GitResolveFileSystem: DEBUG escape hatch for .slnx root; .artifacts in allowed hidden folders
- docs-migrate ServeCommand: use Paths.WorkingDirectoryRoot instead of CWD
- docs-migrate.csproj: add Elastic.Documentation.Tooling project reference
- docs-builder ServeCommand: remove unused System.IO.Abstractions using
- ChunkerTests: 391-line test suite for new chunker behaviour

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Subpixel smoothing made Inter look heavier than the docs type in Figma.

Co-authored-by: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants