From 8d31519c79c59909e663b35698395274e3ca13c3 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Thu, 10 Sep 2026 14:58:01 +0200 Subject: [PATCH 1/3] Show the product mark beside the spec title on API landing pages A catalog click is a full page load. Shared view-transition names let the mark and title move to the heading. Co-Authored-By: Cursor Grok 4.6 Co-authored-by: Cursor --- .../Landing/ApiCatalogView.cshtml | 4 +-- .../Landing/LandingView.cshtml | 8 ++++- .../Landing/LandingViewModel.cs | 4 +++ src/Elastic.ApiExplorer/_CatalogLayout.cshtml | 7 +++++ src/Elastic.ApiExplorer/_Layout.cshtml | 7 +++++ .../Assets/api-docs.css | 15 ++++++++- .../OpenApiGeneratorCatalogSplitTests.cs | 31 +++++++++++++++++++ 7 files changed, 72 insertions(+), 4 deletions(-) diff --git a/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml b/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml index 4597695488..1bd536e04a 100644 --- a/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml +++ b/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml @@ -29,9 +29,9 @@
@if (tile.IconSvg is not null) { - @(new HtmlString(tile.IconSvg)) + @(new HtmlString(tile.IconSvg)) } -

@tile.Title

+

@tile.Title

@tile.Key
diff --git a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml index 602060c30c..b9147656f8 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml +++ b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml @@ -9,7 +9,13 @@ var apiHxAttrs = LayoutModel.HxAttributes; }
-

@Model.ApiInfo.Title

+
+ @if (Model.IconSvg is not null) + { + @(new HtmlString(Model.IconSvg)) + } +

@Model.ApiInfo.Title

+

@Model.RenderMarkdown(Model.ApiInfo.Description)

License: @Model.ApiInfo.License?.Name

Download source: JSON YAML

diff --git a/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs b/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs index b95206132a..9151067e84 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs +++ b/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs @@ -5,6 +5,7 @@ using Elastic.ApiExplorer.Infrastructure; using Elastic.ApiExplorer.Model; using Elastic.ApiExplorer.Operations; +using Elastic.Documentation.Site.Icons; using Microsoft.OpenApi; namespace Elastic.ApiExplorer.Landing; @@ -20,5 +21,8 @@ public class LandingViewModel(ApiRenderContext context) : ApiViewModel(context) public string JsonUrl { get; } = ApiOutputPaths.JsonUrl(context.CurrentNavigation.Url); public string YamlUrl { get; } = ApiOutputPaths.YamlUrl(context.CurrentNavigation.Url); + public string? IconSvg { get; } = ProductIcons.Get(context.Product?.Id ?? context.CurrentApiKey); + public string? ApiKey { get; } = context.CurrentApiKey; + protected override string BreadcrumbCurrentTitle => ApiInfo.Title ?? CurrentNavigationItem.NavigationTitle; } diff --git a/src/Elastic.ApiExplorer/_CatalogLayout.cshtml b/src/Elastic.ApiExplorer/_CatalogLayout.cshtml index a1a948a8f3..a0f2a4ce3d 100644 --- a/src/Elastic.ApiExplorer/_CatalogLayout.cshtml +++ b/src/Elastic.ApiExplorer/_CatalogLayout.cshtml @@ -7,6 +7,13 @@ if (name == GlobalSections.Head) { + } return Task.CompletedTask; } diff --git a/src/Elastic.ApiExplorer/_Layout.cshtml b/src/Elastic.ApiExplorer/_Layout.cshtml index e82215be81..bc8f65569c 100644 --- a/src/Elastic.ApiExplorer/_Layout.cshtml +++ b/src/Elastic.ApiExplorer/_Layout.cshtml @@ -7,6 +7,13 @@ if (name == GlobalSections.Head) { + } return Task.CompletedTask; } diff --git a/src/Elastic.Documentation.Site/Assets/api-docs.css b/src/Elastic.Documentation.Site/Assets/api-docs.css index 7559ea2d03..5776f6f785 100644 --- a/src/Elastic.Documentation.Site/Assets/api-docs.css +++ b/src/Elastic.Documentation.Site/Assets/api-docs.css @@ -2457,7 +2457,8 @@ button.example-response-tab:focus-visible { border-bottom: 1px solid var(--color-grey-20); } -.api-catalog-header h1 { +.api-catalog-header h1, +.api-landing-heading h1 { margin: 0; font-size: var(--text-4xl); font-weight: 700; @@ -2612,3 +2613,15 @@ button.example-response-tab:focus-visible { .api-catalog-card-actions a:focus-visible { text-decoration: underline; } + +.api-landing-heading { + display: flex; + align-items: center; + column-gap: 14px; +} + +.api-landing-icon, +.api-landing-icon svg { + width: auto; + height: 40px; +} diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs index fbf661f5e9..02ebbbc393 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs @@ -84,11 +84,42 @@ public async Task GenerateCatalog_WritesCombinedCatalogFromMultipleEntries() html.Should().Contain("elasticsearch"); html.Should().Contain("href=\"/docs/api/doc/elasticsearch.json\" download"); html.Should().Contain("href=\"/docs/api/doc/elasticsearch.yaml\" download"); + html.Should().Contain("view-transition-name: api-icon-elasticsearch"); + html.Should().Contain("view-transition-name: api-title-elasticsearch"); + html.Should().Contain("@view-transition"); html.Should().NotContain("listing-group-chips"); html.Should().NotContain("markdown-content"); html.Should().NotContain("id=\"pages-nav\""); } + [Fact] + public async Task GenerateProducts_LandingHeading_ShowsSpecTitleAndProductMark() + { + var outputRoot = Path.Join(Paths.WorkingDirectoryRoot.FullName, $"api-catalog-split-{Guid.NewGuid():N}"); + var context = CreateGenerateContext(outputRoot); + using var versionIndexClient = new VersionIndexClient(BaseUri, MultiVersionHandler(), sleep: (_, _) => Task.CompletedTask); + var reader = CreateSequentialReader(SpecDocument("Elasticsearch main")); + var generator = new OpenApiGenerator( + NullLoggerFactory.Instance, + context, + NoopMarkdownStringRenderer.Instance, + versionIndexClient, + reader + ); + + _ = await generator.GenerateProducts(ctx: TestContext.Current.CancellationToken); + + var html = await context + .WriteFileSystem + .File + .ReadAllTextAsync(Path.Join(outputRoot, "api", "doc", "elasticsearch", "index.html"), TestContext.Current.CancellationToken); + html.Should().Contain("api-landing-heading"); + html.Should().Contain("

Elasticsearch main

"); + html.Should().Contain(" Date: Thu, 10 Sep 2026 15:09:30 +0200 Subject: [PATCH 2/3] Drop catalog-to-landing view transitions The morphing names and @view-transition on API layouts added snapshot cost for a transition we do not want. The landing heading still shows the product mark. Co-Authored-By: Cursor Grok 4.6 Co-authored-by: Cursor --- src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml | 4 ++-- src/Elastic.ApiExplorer/Landing/LandingView.cshtml | 4 ++-- src/Elastic.ApiExplorer/Landing/LandingViewModel.cs | 1 - src/Elastic.ApiExplorer/_CatalogLayout.cshtml | 7 ------- src/Elastic.ApiExplorer/_Layout.cshtml | 7 ------- .../OpenApiGeneratorCatalogSplitTests.cs | 7 +------ 6 files changed, 5 insertions(+), 25 deletions(-) diff --git a/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml b/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml index 1bd536e04a..4597695488 100644 --- a/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml +++ b/src/Elastic.ApiExplorer/Landing/ApiCatalogView.cshtml @@ -29,9 +29,9 @@
@if (tile.IconSvg is not null) { - @(new HtmlString(tile.IconSvg)) + @(new HtmlString(tile.IconSvg)) } -

@tile.Title

+

@tile.Title

@tile.Key
diff --git a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml index b9147656f8..f4a74541de 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingView.cshtml +++ b/src/Elastic.ApiExplorer/Landing/LandingView.cshtml @@ -12,9 +12,9 @@
@if (Model.IconSvg is not null) { - @(new HtmlString(Model.IconSvg)) + @(new HtmlString(Model.IconSvg)) } -

@Model.ApiInfo.Title

+

@Model.ApiInfo.Title

@Model.RenderMarkdown(Model.ApiInfo.Description)

License: @Model.ApiInfo.License?.Name

diff --git a/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs b/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs index 9151067e84..a1f136dac9 100644 --- a/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs +++ b/src/Elastic.ApiExplorer/Landing/LandingViewModel.cs @@ -22,7 +22,6 @@ public class LandingViewModel(ApiRenderContext context) : ApiViewModel(context) public string YamlUrl { get; } = ApiOutputPaths.YamlUrl(context.CurrentNavigation.Url); public string? IconSvg { get; } = ProductIcons.Get(context.Product?.Id ?? context.CurrentApiKey); - public string? ApiKey { get; } = context.CurrentApiKey; protected override string BreadcrumbCurrentTitle => ApiInfo.Title ?? CurrentNavigationItem.NavigationTitle; } diff --git a/src/Elastic.ApiExplorer/_CatalogLayout.cshtml b/src/Elastic.ApiExplorer/_CatalogLayout.cshtml index a0f2a4ce3d..a1a948a8f3 100644 --- a/src/Elastic.ApiExplorer/_CatalogLayout.cshtml +++ b/src/Elastic.ApiExplorer/_CatalogLayout.cshtml @@ -7,13 +7,6 @@ if (name == GlobalSections.Head) { - } return Task.CompletedTask; } diff --git a/src/Elastic.ApiExplorer/_Layout.cshtml b/src/Elastic.ApiExplorer/_Layout.cshtml index bc8f65569c..e82215be81 100644 --- a/src/Elastic.ApiExplorer/_Layout.cshtml +++ b/src/Elastic.ApiExplorer/_Layout.cshtml @@ -7,13 +7,6 @@ if (name == GlobalSections.Head) { - } return Task.CompletedTask; } diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs index 02ebbbc393..8d9cd996f5 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs @@ -84,9 +84,6 @@ public async Task GenerateCatalog_WritesCombinedCatalogFromMultipleEntries() html.Should().Contain("elasticsearch"); html.Should().Contain("href=\"/docs/api/doc/elasticsearch.json\" download"); html.Should().Contain("href=\"/docs/api/doc/elasticsearch.yaml\" download"); - html.Should().Contain("view-transition-name: api-icon-elasticsearch"); - html.Should().Contain("view-transition-name: api-title-elasticsearch"); - html.Should().Contain("@view-transition"); html.Should().NotContain("listing-group-chips"); html.Should().NotContain("markdown-content"); html.Should().NotContain("id=\"pages-nav\""); @@ -114,10 +111,8 @@ public async Task GenerateProducts_LandingHeading_ShowsSpecTitleAndProductMark() .File .ReadAllTextAsync(Path.Join(outputRoot, "api", "doc", "elasticsearch", "index.html"), TestContext.Current.CancellationToken); html.Should().Contain("api-landing-heading"); - html.Should().Contain("

Elasticsearch main

"); + html.Should().Contain("

Elasticsearch main

"); html.Should().Contain(" Date: Thu, 10 Sep 2026 16:18:02 +0200 Subject: [PATCH 3/3] fix: assert landing mark, not any svg (per review by @github-actions) The layout sprite always includes an svg, so a bare tag check can pass without the product mark. Co-Authored-By: Cursor Grok 4.6 Co-authored-by: Cursor --- .../OpenApiGeneratorCatalogSplitTests.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs index 8d9cd996f5..c41dd0bcdb 100644 --- a/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs +++ b/tests/Elastic.ApiExplorer.Tests/OpenApiGeneratorCatalogSplitTests.cs @@ -112,7 +112,8 @@ public async Task GenerateProducts_LandingHeading_ShowsSpecTitleAndProductMark() .ReadAllTextAsync(Path.Join(outputRoot, "api", "doc", "elasticsearch", "index.html"), TestContext.Current.CancellationToken); html.Should().Contain("api-landing-heading"); html.Should().Contain("

Elasticsearch main

"); - html.Should().Contain(""); + html.Should().Contain("viewBox=\"8 4.9995 47.7276 54.001\""); } [Fact]