From e9e987e1f176643812027bf4bb5980d731796fe0 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 16 Sep 2026 15:53:22 +0200 Subject: [PATCH 1/5] Hide the API servers page when the spec has none An empty servers page is noise for readers. Presence now follows ReadServers. Co-Authored-By: Cursor Grok 4.6 Co-authored-by: Cursor --- docs/data/openapi/api-explorer.md | 4 ++-- .../Navigation/ApiNavigationBuilder.cs | 4 +++- .../Structural/ApiStructuralPage.cs | 16 +++++++++++++--- .../OpenApiGeneratorMultiVersionTests.cs | 4 ++-- .../StructuralPageTests.cs | 12 ++++++++++++ 5 files changed, 32 insertions(+), 8 deletions(-) diff --git a/docs/data/openapi/api-explorer.md b/docs/data/openapi/api-explorer.md index 4675bfff3f..71aeadb19e 100644 --- a/docs/data/openapi/api-explorer.md +++ b/docs/data/openapi/api-explorer.md @@ -4,7 +4,7 @@ navigation_title: API Explorer # API Explorer -The API Explorer renders OpenAPI specifications as interactive API documentation. When you configure it in your content set, `docs-builder` automatically generates a product landing page, `/authentication` and `/servers` pages, tag and operation pages, request and response schemas, shared type definitions, and inline examples. +The API Explorer renders OpenAPI specifications as interactive API documentation. When you configure it in your content set, `docs-builder` automatically generates a product landing page, an `/authentication` page, a `/servers` page when the spec declares servers, tag and operation pages, request and response schemas, shared type definitions, and inline examples. The assembler also writes a combined **API catalog** at `/docs/api/`: a grid of product cards on its own layout (no API sidebar). Each card opens the HTML landing page and includes REST and category badges plus JSON and YAML downloads. Markdown, JSON, and YAML stay on the product landing page and in the catalog Markdown export. The card shows `info.description`, clamped to three lines. @@ -107,7 +107,7 @@ The following slugs are reserved and cannot be used as child file names: | `group` | Tag landing pages use `/group/` | | `operation` | Operation pages use `/operation/` | | `authentication` | Each API product has an `/authentication` page | -| `servers` | Each API product has a `/servers` page | +| `servers` | Reserved for the `/servers` page when the spec declares servers | Additionally, the slug must not match any operation moniker already generated by the spec. The build fails with a descriptive error if either collision occurs, naming the conflicting file and diff --git a/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs b/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs index 3aa731a627..feff53f57d 100644 --- a/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs +++ b/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs @@ -153,7 +153,9 @@ public LandingNavigationItem CreateNavigation( } } - finalNavigationItems.AddRange(StructuralNavigationItem.Create(context.UrlPathPrefix, apiUrlSuffix, rootNavigation)); + finalNavigationItems.AddRange( + StructuralNavigationItem.Create(context.UrlPathPrefix, apiUrlSuffix, rootNavigation, openApiDocument) + ); // Add existing navigation items (OpenAPI generated content) if (topLevelNavigationItems.Count > 0) diff --git a/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs b/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs index ce15f9b234..f478ee96ca 100644 --- a/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs +++ b/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs @@ -76,11 +76,21 @@ INodeNavigationItem parent public INodeNavigationItem? Parent { get; set; } public int NavigationIndex { get; set; } - public static IReadOnlyList Create(string? urlPathPrefix, string apiUrlSuffix, LandingNavigationItem root) => + public static IReadOnlyList Create( + string? urlPathPrefix, + string apiUrlSuffix, + LandingNavigationItem root, + OpenApiDocument document + ) + { + List items = [ - new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Authentication), root, root), - new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Servers), root, root) + new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Authentication), root, root) ]; + if (StructuralViewModel.ReadServers(document).Count > 0) + items.Add(new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Servers), root, root)); + return items; + } } public class StructuralViewModel(ApiRenderContext context) : ApiViewModel(context) diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs index a5ce73fabf..478e186206 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs @@ -283,7 +283,7 @@ public async Task Generate_WritesDistinctOutputTreesForMainAndReleasedMajors() .File .Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "servers", "index.html")) .Should() - .BeTrue(); + .BeFalse(); context.WriteFileSystem.File.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v9", "index.html")).Should().BeTrue(); context .WriteFileSystem @@ -303,7 +303,7 @@ public async Task Generate_WritesDistinctOutputTreesForMainAndReleasedMajors() .File .Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v8", "servers", "index.html")) .Should() - .BeTrue(); + .BeFalse(); context .WriteFileSystem .File diff --git a/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs b/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs index 01723018e9..511429e507 100644 --- a/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs @@ -4,6 +4,7 @@ using AwesomeAssertions; using Elastic.ApiExplorer.Infrastructure; +using Elastic.ApiExplorer.Landing; using Elastic.ApiExplorer.Structural; using Elastic.Documentation.Navigation; using Elastic.Documentation.Site.FileProviders; @@ -35,6 +36,17 @@ public void CreateNavigation_IncludesAuthenticationAndServersPages() servers.NavigationTitle.Should().Be("Servers"); } + [Fact] + public void Create_NoServers_OmitsServersPage() + { + var document = new OpenApiDocument { Info = new OpenApiInfo { Title = "t", Version = "1" } }; + var root = new LandingNavigationItem("/api/doc/fixture"); + var items = StructuralNavigationItem.Create(urlPathPrefix: null, "fixture", root, document); + + items.Should().ContainSingle(item => item.Model.Kind == ApiStructuralKind.Authentication); + items.Should().NotContain(item => item.Model.Kind == ApiStructuralKind.Servers); + } + [Fact] public void ReadSchemes_MapsFixtureApiKey() { From 73713adb791f309d6ecdb01a055e0e1cd32a1908 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 16 Sep 2026 16:23:39 +0200 Subject: [PATCH 2/5] Remove restated navigation-assembly comment The next lines already add the generated OpenAPI items. Co-Authored-By: Cursor Grok 4.6 Co-authored-by: Cursor --- src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs b/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs index feff53f57d..a8637e7d1c 100644 --- a/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs +++ b/src/Elastic.ApiExplorer/Navigation/ApiNavigationBuilder.cs @@ -157,7 +157,6 @@ public LandingNavigationItem CreateNavigation( StructuralNavigationItem.Create(context.UrlPathPrefix, apiUrlSuffix, rootNavigation, openApiDocument) ); - // Add existing navigation items (OpenAPI generated content) if (topLevelNavigationItems.Count > 0) finalNavigationItems.AddRange(topLevelNavigationItems); else if (rootNavigation.NavigationItems.Count > 0) From aa3839f165e638c2930a9cb7e74b286cfc0b5406 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 16 Sep 2026 16:42:14 +0200 Subject: [PATCH 3/5] Hide the authentication page when the spec has no schemes Bump.sh keeps Authentications in the sidebar only when schemes exist. An empty Authentication page is the same dead end as an empty Servers page. Co-Authored-By: Cursor Grok 4.6 Co-authored-by: Cursor --- docs/data/openapi/api-explorer.md | 4 +- .../Structural/ApiStructuralPage.cs | 12 ++--- .../OpenApiGeneratorMultiVersionTests.cs | 6 +-- .../StructuralPageTests.cs | 48 +++++++++++++++++-- 4 files changed, 56 insertions(+), 14 deletions(-) diff --git a/docs/data/openapi/api-explorer.md b/docs/data/openapi/api-explorer.md index 71aeadb19e..4847aa35ec 100644 --- a/docs/data/openapi/api-explorer.md +++ b/docs/data/openapi/api-explorer.md @@ -4,7 +4,7 @@ navigation_title: API Explorer # API Explorer -The API Explorer renders OpenAPI specifications as interactive API documentation. When you configure it in your content set, `docs-builder` automatically generates a product landing page, an `/authentication` page, a `/servers` page when the spec declares servers, tag and operation pages, request and response schemas, shared type definitions, and inline examples. +The API Explorer renders OpenAPI specifications as interactive API documentation. When you configure it in your content set, `docs-builder` automatically generates a product landing page, an `/authentication` page when the spec declares security schemes, a `/servers` page when the spec declares servers, tag and operation pages, request and response schemas, shared type definitions, and inline examples. The assembler also writes a combined **API catalog** at `/docs/api/`: a grid of product cards on its own layout (no API sidebar). Each card opens the HTML landing page and includes REST and category badges plus JSON and YAML downloads. Markdown, JSON, and YAML stay on the product landing page and in the catalog Markdown export. The card shows `info.description`, clamped to three lines. @@ -106,7 +106,7 @@ The following slugs are reserved and cannot be used as child file names: | `types` | API Explorer uses this path for schema type pages | | `group` | Tag landing pages use `/group/` | | `operation` | Operation pages use `/operation/` | -| `authentication` | Each API product has an `/authentication` page | +| `authentication` | Reserved for the `/authentication` page when the spec declares security schemes | | `servers` | Reserved for the `/servers` page when the spec declares servers | Additionally, the slug must not match any operation moniker already generated by the spec. The diff --git a/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs b/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs index f478ee96ca..2774c3d0cd 100644 --- a/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs +++ b/src/Elastic.ApiExplorer/Structural/ApiStructuralPage.cs @@ -83,10 +83,9 @@ public static IReadOnlyList Create( OpenApiDocument document ) { - List items = - [ - new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Authentication), root, root) - ]; + var items = new List(); + if (StructuralViewModel.HasSchemes(document)) + items.Add(new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Authentication), root, root)); if (StructuralViewModel.ReadServers(document).Count > 0) items.Add(new(urlPathPrefix, apiUrlSuffix, new ApiStructuralPage(ApiStructuralKind.Servers), root, root)); return items; @@ -122,10 +121,11 @@ public static StructuralViewModel Create(ApiStructuralPage page, ApiRenderContex EmptyMessage = "This API does not declare servers." }; + internal static bool HasSchemes(OpenApiDocument document) => document.Components?.SecuritySchemes is { Count: > 0 }; + internal static IReadOnlyList ReadSchemes(ApiRenderContext context) { - var schemes = context.Model.Components?.SecuritySchemes; - if (schemes is not { Count: > 0 }) + if (context.Model.Components?.SecuritySchemes is not { Count: > 0 } schemes) return []; var displays = new List(schemes.Count); diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs index 478e186206..f8edca4c9b 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorMultiVersionTests.cs @@ -277,7 +277,7 @@ public async Task Generate_WritesDistinctOutputTreesForMainAndReleasedMajors() .File .Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "authentication", "index.html")) .Should() - .BeTrue(); + .BeFalse(); context .WriteFileSystem .File @@ -290,14 +290,14 @@ public async Task Generate_WritesDistinctOutputTreesForMainAndReleasedMajors() .File .Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v9", "authentication", "index.html")) .Should() - .BeTrue(); + .BeFalse(); context.WriteFileSystem.File.Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v8", "index.html")).Should().BeTrue(); context .WriteFileSystem .File .Exists(Path.Join(outputRoot, "api", "doc", "elasticsearch", "v8", "authentication", "index.html")) .Should() - .BeTrue(); + .BeFalse(); context .WriteFileSystem .File diff --git a/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs b/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs index 511429e507..324f742ff5 100644 --- a/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/StructuralPageTests.cs @@ -36,17 +36,59 @@ public void CreateNavigation_IncludesAuthenticationAndServersPages() servers.NavigationTitle.Should().Be("Servers"); } + [Fact] + public void Create_EmptyDocument_OmitsAuthenticationAndServersPages() + { + var items = CreateItems(new OpenApiDocument { Info = new OpenApiInfo { Title = "t", Version = "1" } }); + + items.Should().BeEmpty(); + } + + [Fact] + public void Create_NoSchemes_OmitsAuthenticationPage() + { + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "t", Version = "1" }, + Servers = [new OpenApiServer { Url = "https://example.com" }] + }; + var items = CreateItems(document); + + items.Should().ContainSingle(item => item.Model.Kind == ApiStructuralKind.Servers); + items.Should().NotContain(item => item.Model.Kind == ApiStructuralKind.Authentication); + } + [Fact] public void Create_NoServers_OmitsServersPage() { - var document = new OpenApiDocument { Info = new OpenApiInfo { Title = "t", Version = "1" } }; - var root = new LandingNavigationItem("/api/doc/fixture"); - var items = StructuralNavigationItem.Create(urlPathPrefix: null, "fixture", root, document); + var document = new OpenApiDocument + { + Info = new OpenApiInfo { Title = "t", Version = "1" }, + Components = new OpenApiComponents + { + SecuritySchemes = new Dictionary + { + ["apiKey"] = new OpenApiSecurityScheme + { + Type = SecuritySchemeType.ApiKey, + Name = "Authorization", + In = ParameterLocation.Header + } + } + } + }; + var items = CreateItems(document); items.Should().ContainSingle(item => item.Model.Kind == ApiStructuralKind.Authentication); items.Should().NotContain(item => item.Model.Kind == ApiStructuralKind.Servers); } + private static IReadOnlyList CreateItems(OpenApiDocument document) + { + var root = new LandingNavigationItem("/api/doc/fixture"); + return StructuralNavigationItem.Create(urlPathPrefix: null, "fixture", root, document); + } + [Fact] public void ReadSchemes_MapsFixtureApiKey() { From 3ff7bfb42a360df5e9403f18b3a19e70b88e5c13 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Wed, 16 Sep 2026 16:49:57 +0200 Subject: [PATCH 4/5] fix: restore reserved-slug wording for servers (per review by @github-actions) ReservedChildSegments still rejects servers and authentication unconditionally. Co-Authored-By: Cursor Grok 4.6 Co-authored-by: Cursor --- docs/data/openapi/api-explorer.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data/openapi/api-explorer.md b/docs/data/openapi/api-explorer.md index 4847aa35ec..2771b4a11d 100644 --- a/docs/data/openapi/api-explorer.md +++ b/docs/data/openapi/api-explorer.md @@ -106,8 +106,8 @@ The following slugs are reserved and cannot be used as child file names: | `types` | API Explorer uses this path for schema type pages | | `group` | Tag landing pages use `/group/` | | `operation` | Operation pages use `/operation/` | -| `authentication` | Reserved for the `/authentication` page when the spec declares security schemes | -| `servers` | Reserved for the `/servers` page when the spec declares servers | +| `authentication` | Reserved for API Explorer and cannot be used as a child file slug | +| `servers` | Reserved for API Explorer and cannot be used as a child file slug | Additionally, the slug must not match any operation moniker already generated by the spec. The build fails with a descriptive error if either collision occurs, naming the conflicting file and From 3aca1712685951535555adf37ae31908e461abb4 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Tue, 22 Sep 2026 15:23:07 +0200 Subject: [PATCH 5/5] Stop expecting empty structural pages in the tag-group nav test The spec declares no schemes and no servers, so those pages stay out of the nav. Co-Authored-By: Grok 4.7 Co-authored-by: Cursor --- tests/Elastic.ApiExplorer.Tests/ApiNavParityTests.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/tests/Elastic.ApiExplorer.Tests/ApiNavParityTests.cs b/tests/Elastic.ApiExplorer.Tests/ApiNavParityTests.cs index 2afd5d94f5..d146a1ae23 100644 --- a/tests/Elastic.ApiExplorer.Tests/ApiNavParityTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/ApiNavParityTests.cs @@ -60,11 +60,7 @@ public async Task CreateNavigation_XTagGroupsWithMultipleGroups_Default_TagsHang "/api/doc/elasticsearch/group/endpoint-search" ); - var structural = navigation.NavigationItems.OfType().ToList(); - structural.Should().Contain( - item => item.Model.Kind == ApiStructuralKind.Authentication && item.Url == "/api/doc/elasticsearch/authentication" - ); - structural.Should().Contain(item => item.Model.Kind == ApiStructuralKind.Servers && item.Url == "/api/doc/elasticsearch/servers"); + navigation.NavigationItems.OfType().Should().BeEmpty(); } [Fact]