Repository navigation
Link to API pages with cross-links and publish them in links.json - #4731
Merged
Merged
Conversation
A toc entry such as `api: elasticsearch` now mounts a floating section: a set of pages that owns its URL root and sidebar and is not made of Markdown files. A provider resolves the section before navigation is built, and it renders as an island. `CreateLinkReference()` skips these nodes instead of casting them to `MarkdownFile`. No provider is registered yet, so a build that uses the entry reports an error. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…entries The site assembler dropped every root child of a mounted docset and warned that it was undeclared, so an `api:` entry vanished from the assembled navigation. Floating section roots now carry a marker interface and the assembler keeps them. An `api:` entry that also sets `children:`, `file:` or other keys now reports an error instead of silently dropping them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A floating section root now exists as soon as the toc mounts it, with its landing URL and title, so layout code can link to it. The children come later from `ResolveAsync`, which `DocumentationSet.ResolveFloatingSectionsAsync` runs before generation. The children keep their own numbering, so filling them never renumbers the surrounding navigation. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The narrative root copied only leaf items into the site tree, so an `- api: <key>` entry in its toc resolved and then vanished. The root now keeps floating section roots next to its leaves. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ResolveDirectoryTree now fills floating sections, so DocumentationGenerator, the assembler and the codex build see their pages without a separate call. The test goes through ResolveDirectoryTree and checks resolution runs once. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An `api: <key>` toc entry now resolves to an API section. Its root exists when the toc is read, and `ResolveAsync` fills it with the version trees before generation. The generator builds each API's trees once and renders from them, so mounting a section adds no second fetch or build. Only the current version is visible. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The docset now fills floating sections itself, so a build run with `--skip-api` resolves an `api:` entry without a generator. The section stays empty and hidden instead of failing. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ip-api A spec that fails to build no longer stops the Markdown build: the section reports the error and hides. The section also hides when no main version resolved, and a hidden API no longer appears in the section dropdown. The generation state records --skip-api, so toggling it triggers a full build. The docs now say that only the isolated build accepts api: entries. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The floating section no longer emits its own error when tree building fails, because the generator awaits the same cached task and already reports it with the stack trace. The no-main test now makes main unreadable without throwing, so it proves released versions loaded. A new build-twice test covers toggling --skip-api over unchanged pages. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A mounted API now publishes its pages in `links.json` under `floating`, one entry per page path with a bit mask of the versions that contain it. A cross-link such as `elasticsearch://api/elasticsearch/v8/operation/operation-search` resolves through hash lookups and a bit test, and a page missing in that version is an error. The resolver builds the final URL from the site root, because API pages sit outside every repository path prefix. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An alias now belongs to the versions that list it, so a link to an alias in a version without it fails, and an alias can name different pages in different versions. A cross-link to an API page keeps its #fragment. CodexAwareUriResolver sends API pages to the published site instead of a repository path. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A fragment stays on the resolved URL and the build does not check the anchor. Aliases of a collapsed operation resolve only in the versions that list them. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A floating link section no longer assumes how a kind routes its URLs. Each version names its root, and whether it is hidden or current, so a reader joins a root and a page path and never adds a `v` segment. A cross-link picks a version by its name, and aliases are listed under their canonical path, which writes that path once however many aliases it has. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
An API page called `main` or `8` was read as a version, so the link reached the landing page. A page of the current version now takes precedence, so adding a version never changes where an existing link points. The current version's bit is cached instead of scanned for on every unversioned link. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The lookups of a section are built on first use. The copy that a `with` expression makes carried them over, so replacing the versions or the aliases kept answering from the old data, and a test failed only when another test had used the shared fixture first. A copy now keeps the data and none of the lookups. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This was referenced Nov 5, 2026
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The squash merge of the previous PR left two copies of the test, which stops the test project compiling on main. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Contributor
Docs preview (local build)Handbook preview: https://docs-v3-preview.elastic.dev/elastic/docs-builder/pull/4731/ Sample changed pages: |
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.
A docset that mounts an API now publishes its pages in
links.json, and a cross-link such aselasticsearch://api/elasticsearch/8/operation/operation-searchresolves to the page or fails the build when the page is missing in that version. Docsets that mount no API publish the samelinks.jsonas before.Affects: Links & redirects, Authoring, API reference
Prompt summary: Make API pages part of the navigation tree in isolated and assembler builds, so links to them can be validated like any other cross-link. Keep the published form compact and every lookup O(1), however many API versions a section has. This is the third step of the design in RFC: Floating sections in the navigation tree.
Why
An API page is not a Markdown file, so it is not in
links.json. An author cannot cross-link to one, and a hard-codedhttps://www.elastic.co/docs/api/doc/...link is not checked in an isolated build. One API can have several versions of thousands of pages, and the same page paths repeat in every version.What
One entry per page, with its versions
RepositoryLinksgains an optionalfloatingproperty, keyed by{kind}/{key}. For each API it lists every page path once with a bit mask of the versions that contain it. The property is left out when empty, so existinglinks.jsonfiles do not change.Each version states its root
The section does not name a routing pattern. Each version carries its
name, therootof its URLs, and whether it ishiddenorcurrent. A reader joins a root and a page path, and never adds avsegment or knows how the API routes. The API Explorer builds the roots withApiUrlBuilder, the same code that routes the pages.Aliases under their canonical page
The former per-operation paths of a collapsed operation are listed under the canonical path, per version, so the canonical path is written once however many aliases it has. The reader builds the reverse lookup once, so finding the canonical page of an alias stays one lookup.
The lookup
CrossLinkResolversplitsapi/{key}[/{version}]/{page}, finds the section by key, maps the version to its bit and tests the mask of the page. The version is the exact name of a version, such as8ormain. A page of the current version with the same name wins over the version, so adding a version never changes where a link points. Without it, the link reaches the current version, and a section with no current version does not guess one. Every step is a hash lookup or a bit test, so the cost does not grow with the number of versions.Resolving to a site path
API pages sit at the site root, outside every repository
path_prefix.IUriEnvironmentResolver.ResolveSitePathbuilds the URL from the site root. An assembler build links to the page on the site it builds, and an isolated build links to the published page onhttps://www.elastic.co/docs/. Codex builds link to the published page too, and a#fragmentstays on the URL unchecked.Emission and docs
IFloatingSection.ToLinkSectionbuilds the section from the same per-version page names the version switcher uses. The syntax is documented indocs/data/openapi/api-explorer.md.Example
The excerpts come from a real build of
docs/with- api: elasticsearchin the toc, trimmed to a few entries. Thealiasesentry is made up: that build had no collapsed operations.A docset that mounts an API gets one extra property in
links.json. Everything else in the file is unchanged.{ "origin": { "...": "unchanged" }, "url_path_prefix": "", "links": { "index.md": {}, "...": {} }, "cross_links": [], "floating": { "api/elasticsearch": { "versions": [ { "name": "8", "root": "api/doc/elasticsearch/v8", "hidden": true }, { "name": "9", "root": "api/doc/elasticsearch/v9", "hidden": true }, { "name": "main", "root": "api/doc/elasticsearch", "current": true } ], "pages": { "": 7, "authentication": 7, "operation/operation-search": 7, "operation/operation-cat-circuit-breaker": 6, "operation/operation-autoscaling-get-autoscaling-policy": 1 }, "aliases": { "8": { "operation/operation-search": ["operation/operation-search-1", "operation/operation-search-2"] } } } } }To rebuild a URL, join the
rootof a version and a page path. The landing page has the empty path, and its URL is the root. The position of a version inversionsis its bit, so8is bit 0,9is bit 1 andmainis bit 2. A mask says which versions contain the page:operation/operation-search71118,9,mainoperation/operation-cat-circuit-breaker61109,main(added in 9.x)operation/operation-autoscaling-get-autoscaling-policy10018only (gone in newer versions)""(the landing page)71118,9,main8and9arehidden, so they are reachable by link but out of the sitemap, andmainis the visible,currentversion. In that build, 579 pages exist in every version, 44 only in9andmain, and 11 only in8, so 634 entries cover 1,902 page and version combinations. All 1,902 match the files the build rendered, found through the stated roots. A docset that mounts no API has nofloatingproperty at all.Cross-links resolve against that section. These results come from running the real resolver on the real
links.jsonof that build, with the repository namedocs-builder:docs-builder://api/elasticsearch/operation/operation-searchhttps://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-searchdocs-builder://api/elasticsearch/8/operation/operation-searchhttps://www.elastic.co/docs/api/doc/elasticsearch/v8/operation/operation-searchdocs-builder://api/elasticsearch/main/operation/operation-searchhttps://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-searchdocs-builder://api/elasticsearchhttps://www.elastic.co/docs/api/doc/elasticsearchdocs-builder://api/elasticsearch/operation/operation-search#responseshttps://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-search#responsesdocs-builder://api/elasticsearch/operation/operation-autoscaling-get-autoscaling-policy'operation/operation-autoscaling-get-autoscaling-policy' is not a page of 'api/elasticsearch' in the current version.docs-builder://api/elasticsearch/8/operation/operation-cat-circuit-breaker'operation/operation-cat-circuit-breaker' is not a page of 'api/elasticsearch' in version '8'.docs-builder://api/elasticsearch/v8/operation/operation-search'v8/operation/operation-search' is not a page of 'api/elasticsearch' in the current version.The version is named8, and no reader guesses avprefix.Stack: 3 of 5, on top of #4718.
Out of scope: the assembler wiring,
navigation.ymlmounting, replacing the link check in #4653, and anchor checking. Only a docset that mounts an API with anapi:toc entry publishes anything, and no docset does yet.Verify
🤖 Generated with Claude Code