From d1a5b670353fcf217f339e55707ca2233ac76c5d Mon Sep 17 00:00:00 2001 From: Andrea De Pirro Date: Wed, 21 Oct 2026 19:33:47 +0200 Subject: [PATCH 1/6] Add a finder to the all-APIs landing page Readers could only scroll or use browser find to locate an API or endpoint on the catalog page. The title band now hosts the same `navigation-search` box as the API sidebar, which looks up operations across every API. It follows the sidebar rule: assembler builds only, not air-gapped. Co-Authored-By: Claude Sonnet 5.5 --- docs/data/openapi/api-explorer.md | 4 +- .../Landing/ApiCatalogView.cshtml | 8 ++++ .../Assets/api-docs.css | 8 ++++ .../OpenApiGeneratorCatalogSplitTests.cs | 42 +++++++++++++++++-- 4 files changed, 57 insertions(+), 5 deletions(-) diff --git a/docs/data/openapi/api-explorer.md b/docs/data/openapi/api-explorer.md index 92e39e23e6..c23cc9debd 100644 --- a/docs/data/openapi/api-explorer.md +++ b/docs/data/openapi/api-explorer.md @@ -10,6 +10,8 @@ Authored API Markdown pages can use [`meta_title`](../../syntax/frontmatter.md#m The assembler also writes a combined **Elastic APIs** page at `/docs/api/`, on its own layout with no API sidebar. The cards match the ones on the [docs landing page](https://www.elastic.co/docs): a rounded box with the product icon, a bold title, and a short description. The whole card is one link to the API. The title sits on a full-width band with one line of text under it that says which deployments the APIs cover. Two small labels group the APIs into two full-width rows. **Elastic Stack** shows Elasticsearch, Kibana and Logstash as three equal large cards, with the Serverless variants of Elasticsearch and Kibana as small compact cards directly under their parents. **Orchestration** shows the five Elastic Cloud APIs three across, in the same compact card as the Serverless variants but without the attachment. An API that is in neither group follows them, sorted by title. Each API appears once. If only one group has APIs, the label is dropped. If fewer than two of Elasticsearch, Kibana and Logstash are present, the group has no larger cards. +Under the line of text sits the same finder as in the API sidebar, labelled **Press ❲/❳ to find API**. It looks up operations across every API, so you can jump to an endpoint without knowing which product it belongs to. Press / anywhere on the page, outside a text field, to jump to it. Like the sidebar finder, it is omitted on isolated and air-gapped builds. + The cards for APIs that are in neither group show the API's deployments as small pills taken from `catalog.categories`. They use the same look as `applies_to` badges and the same names (**Self-managed**, **ECE**, **ECH**, **Serverless**), but show the name only: the catalog knows the deployment, not a lifecycle or version. The grouped cards leave them out, and an API with no categories shows no pills. The description is a short plain-text summary of `info.description` (the first paragraph with Markdown removed). JSON, YAML, and Markdown downloads stay on each API's landing page and in the `/docs/api.md` export. The API title comes from `info.title` in the spec, so fix naming and description copy in the spec. @@ -152,7 +154,7 @@ Each product key in the `api:` block must have **exactly one** entry, with **exa `spec:`. The build fails if a product sequence is empty or has more than one entry. Multiple specs per product are not currently supported. -On assembler builds a bar under the site header says which API you are reading: `[logo] Elasticsearch API ⇅ [main ▾]`. The API name opens a switcher that lists every API whose spec loaded (each with its logo, plus an **All APIs** link back to the catalog), and the version picker sits right beside it. Each switcher row uses that spec's title (`info.title`), the same text as the landing page heading. On the right, the **Download OpenAPI spec** button downloads the whole spec as JSON or YAML; on narrow screens it shows only its icon. The page itself starts with the full breadcrumb, which begins at Docs. The bar looks the same whether or not `navigation-preview` is on. The catalog page has no API, so it has no bar. +On assembler builds a bar under the site header says which API you are reading: `[logo] Elasticsearch API ⇅ [main ▾]`. The API name opens a switcher that lists every API whose spec loaded (each with its logo, plus an **All APIs** link back to the catalog), and the version picker sits right beside it. Each switcher row uses that spec's title (`info.title`), the same text as the landing page heading. On the right, the **Download OpenAPI spec** button downloads the whole spec as JSON or YAML; on narrow screens it shows only its icon. The page itself starts with the full breadcrumb, which begins at Docs. The bar looks the same whether or not `navigation-preview` is on. The catalog page has no API, so it has no bar; its finder sits in the title band instead. The left navigation starts with a finder, labelled **Press ❲/❳ to find API**. It looks up operations by title across every API, not only the one you are reading, and shows the matches in a list; it does not filter the navigation tree. It is not the site-wide search. Press / anywhere on the page, outside a text field, to jump to the box, as on GitHub. The shortcut is off on narrow screens, where the navigation sits in a drawer. Isolated and air-gapped builds omit it. Isolated builds have no site header and no bar, so they put the API name, switcher and version picker at the top of the left navigation instead. Markdown docs pages do not get the filter box back. diff --git a/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml b/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml index ccbb3c3493..a6dbf9ff42 100644 --- a/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml +++ b/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml @@ -18,6 +18,10 @@

Reference

@ApiCatalog.PageTitle

@ApiCatalog.PageLede

+ @if (LayoutModel.ShowJumpToPage) + { +
+ } return Task.CompletedTask; @@ -31,6 +35,10 @@

@ApiCatalog.PageTitle

@ApiCatalog.PageLede

+ @if (LayoutModel.ShowJumpToPage) + { +
+ } diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index bf9e70ba5d..c8d08a07fa 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -3199,6 +3199,14 @@ button.example-response-tab:focus-visible { text-wrap: balance; } +/* The finder is the same box as the API sidebar's, so it looks up operations across every API. It sits + under the lede, as wide as the lede, and its results open below it. */ +.api-catalog .api-catalog-finder { + max-width: 36rem; + margin: 20px auto 0; + text-align: left; +} + @media (max-width: 640px) { .api-catalog .hub-hero.api-catalog-hero { padding: 36px 0 32px; diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs index 386640b9df..41a935c203 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs @@ -396,7 +396,7 @@ public async Task GenerateCatalog_CategoriesDeclared_RendersOneListWithDeploymen } [Test] - public async Task GenerateCatalog_RendersTheTitleBandOutsideTheContainerAndNoSearchField() + public async Task GenerateCatalog_RendersTheTitleBandOutsideTheContainer() { var outputRoot = Path.Join(Paths.WorkingDirectoryRoot.FullName, $"api-catalog-split-{Guid.NewGuid():N}"); var context = CreateGenerateContext(outputRoot); @@ -423,9 +423,38 @@ await generator.GenerateCatalog( html.IndexOf("id=\"content-container\"", StringComparison.Ordinal), "the band is rendered outside the width-limited container" ); - html.Should().NotContain("navigation-search", "the catalog has no search field"); } + [Test] + [Arguments(BuildType.Assembler, true)] + [Arguments(BuildType.Isolated, false)] + public async Task GenerateCatalog_Finder_AppearsOnlyWhereTheSidebarHasOne(BuildType buildType, bool expected) + { + var outputRoot = Path.Join(Paths.WorkingDirectoryRoot.FullName, $"api-catalog-split-{Guid.NewGuid():N}"); + var context = CreateGenerateContext(outputRoot, buildType: buildType); + using var versionIndexClient = new VersionIndexClient(BaseUri, MultiVersionHandler(), sleep: (_, _) => Task.CompletedTask); + var generator = new OpenApiGenerator(NullLoggerFactory.Instance, context, NoopMarkdownStringRenderer.Instance, versionIndexClient); + + await generator.GenerateCatalog( + [new("elasticsearch", "Elasticsearch", "/docs/api/doc/elasticsearch", "elasticsearch")], + TestContext.Current!.Execution.CancellationToken + ); + + var html = await context + .WriteFileSystem + .File + .ReadAllTextAsync(Path.Join(outputRoot, "api", "index.html"), TestContext.Current!.Execution.CancellationToken); + if (expected) + { + html.Should().Contain(""); + Count(html, " html.Split(value).Length - 1; + [Test] public async Task GenerateCatalog_CloudHostedAndEnterprise_ShowCloudMarksNotTheGenericGlyph() { @@ -450,7 +479,12 @@ public async Task GenerateCatalog_CloudHostedAndEnterprise_ShowCloudMarksNotTheG html.Should().NotContain("M13.5 5 10.5 19", "that is the generic fallback glyph"); } - private static BuildContext CreateGenerateContext(string outputRoot, string? docsetYaml = null, string? displayName = null) + private static BuildContext CreateGenerateContext( + string outputRoot, + string? docsetYaml = null, + string? displayName = null, + BuildType buildType = BuildType.Isolated + ) { var collector = new DiagnosticsCollector([]); var stack = TestHelpers.CreateStackVersionsConfiguration(currentMajor: 9); @@ -501,7 +535,7 @@ private static BuildContext CreateGenerateContext(string outputRoot, string? doc ), configurationContext ) - { UrlPathPrefix = "docs" }; + { UrlPathPrefix = "docs", BuildType = buildType }; } private static OpenApiDocument SpecDocument(string title) => From c2318c7ab064980eb4247252570fb60d5c23296c Mon Sep 17 00:00:00 2001 From: Andrea De Pirro Date: Wed, 21 Oct 2026 19:37:25 +0200 Subject: [PATCH 2/6] Style the landing finder in the navigation-preview hero The preview hero sits outside `.api-catalog`, so the scoped `.api-catalog-finder` rule never applied there. The rule now targets the finder alone. Co-Authored-By: Claude Sonnet 5.5 --- src/Elastic.Documentation.Site/Assets/api-docs.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index c8d08a07fa..da0ec67b99 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -3200,8 +3200,9 @@ button.example-response-tab:focus-visible { } /* The finder is the same box as the API sidebar's, so it looks up operations across every API. It sits - under the lede, as wide as the lede, and its results open below it. */ -.api-catalog .api-catalog-finder { + under the lede, as wide as the lede, and its results open below it. Both heroes use it, and the + navigation-preview one sits outside `.api-catalog`, so the rule stands alone. */ +.api-catalog-finder { max-width: 36rem; margin: 20px auto 0; text-align: left; From 94a4c903f1cb3870e85f32f6fe3f23d16f74f3df Mon Sep 17 00:00:00 2001 From: Andrea De Pirro Date: Wed, 21 Oct 2026 21:19:35 +0200 Subject: [PATCH 3/6] Align the landing finder with the hero text The preview hero is left-aligned, but the finder's auto margins centred it in the band. It now keeps the hero's alignment, and the custom element fills its container. Co-Authored-By: Claude Sonnet 5.5 --- src/Elastic.Documentation.Site/Assets/api-docs.css | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index da0ec67b99..322893376d 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -3208,6 +3208,16 @@ button.example-response-tab:focus-visible { text-align: left; } +.api-catalog-finder navigation-search { + display: block; + width: 100%; +} + +/* The navigation-preview hero is left-aligned, so the finder lines up with its title and lede. */ +.landing__hero-text .api-catalog-finder { + margin-inline: 0; +} + @media (max-width: 640px) { .api-catalog .hub-hero.api-catalog-hero { padding: 36px 0 32px; From 1aeba8b6af39437ef2b81c1fe8cf8b47de8b448a Mon Sep 17 00:00:00 2001 From: Andrea De Pirro Date: Thu, 22 Oct 2026 10:09:46 +0200 Subject: [PATCH 4/6] Let the landing finder fill its container `EuiInputPopover` caps itself at 400px unless it is full width, so the box sat at the left edge of the finder while the title and lede were centred. The cap is lifted inside `.api-catalog-finder` only, so the sidebar box is unchanged. Co-Authored-By: Claude Sonnet 5.5 --- src/Elastic.Documentation.Site/Assets/api-docs.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index 322893376d..332c79d486 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -3213,6 +3213,12 @@ button.example-response-tab:focus-visible { width: 100%; } +/* EUI caps an input popover at 400px unless it is told to be full width. The cap left the box at the left + edge of the finder instead of filling it. */ +.api-catalog-finder .euiInputPopover { + max-width: none; +} + /* The navigation-preview hero is left-aligned, so the finder lines up with its title and lede. */ .landing__hero-text .api-catalog-finder { margin-inline: 0; From 779994acb0536feff20e95c0b800b96a4428de2f Mon Sep 17 00:00:00 2001 From: Andrea De Pirro Date: Thu, 22 Oct 2026 10:39:58 +0200 Subject: [PATCH 5/6] Reserve the landing finder's height while it loads The box is rendered by script, so the cards below moved down when it appeared. The container now holds the input's 34px height from the first paint. Co-Authored-By: Claude Sonnet 5.5 --- src/Elastic.Documentation.Site/Assets/api-docs.css | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index 332c79d486..b8d7fe965d 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -3203,6 +3203,9 @@ button.example-response-tab:focus-visible { under the lede, as wide as the lede, and its results open below it. Both heroes use it, and the navigation-preview one sits outside `.api-catalog`, so the rule stands alone. */ .api-catalog-finder { + /* The box is added by script. This is its height (20px line, 12px padding, 2px border), so the cards + below do not jump when it loads. */ + min-height: 34px; max-width: 36rem; margin: 20px auto 0; text-align: left; From 64b17d417df64b4df5c37db9eab9f26cd5a5333f Mon Sep 17 00:00:00 2001 From: Andrea De Pirro Date: Thu, 22 Oct 2026 10:40:34 +0200 Subject: [PATCH 6/6] Reserve the finder's height on the API sidebar too The sidebar box is rendered by script as well, so the navigation tree moved down when it appeared. The min-height now sits on `navigation-search[variant='sidebar']`, which covers the sidebar and the landing page with one rule. Co-Authored-By: Claude Sonnet 5.5 --- src/Elastic.Documentation.Site/Assets/api-docs.css | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index b8d7fe965d..19f39ad9b5 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -3203,16 +3203,19 @@ button.example-response-tab:focus-visible { under the lede, as wide as the lede, and its results open below it. Both heroes use it, and the navigation-preview one sits outside `.api-catalog`, so the rule stands alone. */ .api-catalog-finder { - /* The box is added by script. This is its height (20px line, 12px padding, 2px border), so the cards - below do not jump when it loads. */ - min-height: 34px; max-width: 36rem; margin: 20px auto 0; text-align: left; } -.api-catalog-finder navigation-search { +/* The box is added by script. min-height is its height (20px line, 12px padding, 2px border), so what sits + below it, the sidebar tree or the catalog cards, does not jump when it loads. */ +navigation-search[variant='sidebar'] { display: block; + min-height: 34px; +} + +.api-catalog-finder navigation-search { width: 100%; }