From 21e88aaefa4798fb2994492eb55e6357692d46d1 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Thu, 16 Apr 2026 17:10:48 +0200 Subject: [PATCH 01/33] feat(nav-v2): add section: item type for independent nav islands Introduces the `section:` YAML item type in navigation-v2.yml. Each section owns an independent sidebar tree and optionally appears as a tab in the secondary nav bar. Sections marked `isolated: true` render with a back arrow instead of a top bar tab. Key changes: - SectionNavV2Item record + YAML parsing in NavV2FileYamlConverter - SectionNavigationNode nav type + NavigationSection data carrier - SiteNavigationV2 builds sections, URL-to-section lookup - GlobalNavigationHtmlWriter renders per-section sidebar (cached) - NavigationRenderResult carries section metadata - _SecondaryNav.cshtml is now data-driven (falls back to static V1) - _TocTree.cshtml shows back arrow for isolated sections - PlaceholderPageWriter groups by section for per-section nav Co-Authored-By: Claude Opus 4.6 (1M context) --- src/Elastic.Codex/Page/Index.cshtml | 2 + .../Toc/NavigationV2File.cs | 25 ++++ .../V2/NavigationSection.cs | 17 +++ .../V2/SectionNavigationNode.cs | 77 ++++++++++ .../V2/SiteNavigationV2.cs | 86 ++++++++++- .../Layout/_SecondaryNav.cshtml | 88 +++++++----- .../Navigation/INavigationHtmlWriter.cs | 9 +- .../Navigation/NavigationViewModel.cs | 3 + .../Navigation/_TocTree.cshtml | 11 ++ src/Elastic.Documentation.Site/_ViewModels.cs | 7 + src/Elastic.Markdown/HtmlWriter.cs | 2 + src/Elastic.Markdown/Page/Index.cshtml | 2 + src/Elastic.Markdown/Page/IndexViewModel.cs | 7 + .../Building/PlaceholderPageWriter.cs | 33 +++-- .../Navigation/GlobalNavigationHtmlWriter.cs | 136 +++++++++++++----- 15 files changed, 419 insertions(+), 86 deletions(-) create mode 100644 src/Elastic.Documentation.Navigation/V2/NavigationSection.cs create mode 100644 src/Elastic.Documentation.Navigation/V2/SectionNavigationNode.cs diff --git a/src/Elastic.Codex/Page/Index.cshtml b/src/Elastic.Codex/Page/Index.cshtml index a7f1f08d7e..af56abd0b0 100644 --- a/src/Elastic.Codex/Page/Index.cshtml +++ b/src/Elastic.Codex/Page/Index.cshtml @@ -49,6 +49,8 @@ Previous = Model.PreviousDocument, Next = Model.NextDocument, NavigationHtml = Model.NavigationHtml, + NavV2Sections = Model.NavV2Sections, + ActiveSectionId = Model.ActiveSectionId, UrlPathPrefix = Model.UrlPathPrefix, GithubEditUrl = Model.GithubEditUrl, MarkdownUrl = Model.MarkdownUrl, diff --git a/src/Elastic.Documentation.Configuration/Toc/NavigationV2File.cs b/src/Elastic.Documentation.Configuration/Toc/NavigationV2File.cs index 9a61e1396f..e43453a081 100644 --- a/src/Elastic.Documentation.Configuration/Toc/NavigationV2File.cs +++ b/src/Elastic.Documentation.Configuration/Toc/NavigationV2File.cs @@ -27,6 +27,18 @@ IReadOnlyList Children /// public record PageNavV2Item(Uri? Page, string? Title) : INavV2Item; +/// +/// A top-level section that owns an independent sidebar tree and (optionally) a tab in the +/// secondary nav bar. When is true the section does not appear +/// in the top bar and renders with a back-arrow instead. +/// +public record SectionNavV2Item( + string Label, + string Url, + bool Isolated, + IReadOnlyList Children +) : INavV2Item; + /// /// A folder node — has a title and children, with an optional page: URI. /// When is set, the header is a real clickable link; otherwise it renders @@ -110,6 +122,19 @@ private static IReadOnlyList ReadItemList(IParser parser, ObjectDese parser.SkipThisAndNestedEvents(); } + if (dict.TryGetValue("section", out var sectionVal) && sectionVal is string sectionStr) + { + var sectionUrl = dict.TryGetValue("url", out var suVal) && suVal is string suStr ? suStr : "/"; + var isolated = dict.TryGetValue("isolated", out var isoVal) + && isoVal is string isoStr + && bool.TryParse(isoStr, out var isoBool) + && isoBool; + var sectionChildren = dict.TryGetValue("children", out var sch) && sch is IReadOnlyList sChildList + ? sChildList + : []; + return new SectionNavV2Item(sectionStr, sectionUrl, isolated, sectionChildren); + } + if (dict.TryGetValue("label", out var labelVal) && labelVal is string labelStr) { var expanded = dict.TryGetValue("expanded", out var expVal) diff --git a/src/Elastic.Documentation.Navigation/V2/NavigationSection.cs b/src/Elastic.Documentation.Navigation/V2/NavigationSection.cs new file mode 100644 index 0000000000..41c99ee76a --- /dev/null +++ b/src/Elastic.Documentation.Navigation/V2/NavigationSection.cs @@ -0,0 +1,17 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +namespace Elastic.Documentation.Navigation.V2; + +/// +/// Lightweight data carrier for a navigation section, used by the rendering layer +/// to drive the secondary nav bar tabs and resolve which sidebar to show. +/// +public record NavigationSection( + string Id, + string Label, + string Url, + bool Isolated, + IReadOnlyList NavigationItems +); diff --git a/src/Elastic.Documentation.Navigation/V2/SectionNavigationNode.cs b/src/Elastic.Documentation.Navigation/V2/SectionNavigationNode.cs new file mode 100644 index 0000000000..2e0012bf75 --- /dev/null +++ b/src/Elastic.Documentation.Navigation/V2/SectionNavigationNode.cs @@ -0,0 +1,77 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using Elastic.Documentation.Extensions; + +namespace Elastic.Documentation.Navigation.V2; + +/// +/// A top-level section that owns an independent sidebar nav tree. +/// Unlike , a section has a real URL (the tab link) +/// and an flag that controls whether it appears in the top bar. +/// +public class SectionNavigationNode : INodeNavigationItem +{ + private readonly SectionIndexLeaf _index; + + public SectionNavigationNode( + string label, + string url, + bool isolated, + IReadOnlyCollection children, + INodeNavigationItem? parent + ) + { + Id = ShortId.Create("section", label); + NavigationTitle = label; + Url = url; + Isolated = isolated; + NavigationItems = children; + Parent = parent; + NavigationRoot = parent?.NavigationRoot!; + _index = new SectionIndexLeaf(this); + } + + /// Whether this section is excluded from the top bar and renders with a back arrow. + public bool Isolated { get; } + + /// + public string Id { get; } + + /// + public string Url { get; } + + /// + public string NavigationTitle { get; } + + /// + public IRootNavigationItem NavigationRoot { get; } + + /// + public INodeNavigationItem? Parent { get; set; } + + /// + public bool Hidden => false; + + /// + public int NavigationIndex { get; set; } + + /// + public ILeafNavigationItem Index => _index; + + /// + public IReadOnlyCollection NavigationItems { get; } + + private sealed class SectionIndexLeaf(SectionNavigationNode owner) + : ILeafNavigationItem, INavigationModel + { + public INavigationModel Model => this; + public string Url => owner.Url; + public string NavigationTitle => owner.NavigationTitle; + public IRootNavigationItem NavigationRoot => owner.NavigationRoot; + public INodeNavigationItem? Parent { get; set; } = owner; + public bool Hidden => true; + public int NavigationIndex { get; set; } + } +} diff --git a/src/Elastic.Documentation.Navigation/V2/SiteNavigationV2.cs b/src/Elastic.Documentation.Navigation/V2/SiteNavigationV2.cs index 8a82d44bef..6f5955d381 100644 --- a/src/Elastic.Documentation.Navigation/V2/SiteNavigationV2.cs +++ b/src/Elastic.Documentation.Navigation/V2/SiteNavigationV2.cs @@ -10,14 +10,19 @@ namespace Elastic.Documentation.Navigation.V2; /// -/// Extends with a V2 label-structured sidebar tree derived from +/// Extends with a V2 section-structured sidebar derived from /// navigation-v2.yml. Content is built at the same URL paths as V1 (the original /// is passed to the base constructor unchanged). -/// Only the sidebar presentation changes — exposes the -/// label/placeholder hierarchy used by _TocTreeNavV2.cshtml. +/// +/// Top-level section: items become independent nav trees (). +/// Each section drives a tab in the secondary nav bar and its own sidebar. +/// Sections marked isolated: true do not appear in the top bar and render with a back arrow. +/// /// public class SiteNavigationV2 : SiteNavigation { + private readonly Dictionary _urlToSection = new(StringComparer.OrdinalIgnoreCase); + public SiteNavigationV2( NavigationV2File v2File, SiteNavigationFile originalFile, @@ -25,15 +30,69 @@ public SiteNavigationV2( IReadOnlyCollection documentationSetNavigations, string? sitePrefix ) : base(originalFile, context, documentationSetNavigations, sitePrefix) - => V2NavigationItems = BuildV2Items(v2File.Nav, Nodes, this, sitePrefix ?? string.Empty); + { + var prefix = sitePrefix ?? string.Empty; + V2NavigationItems = BuildV2Items(v2File.Nav, Nodes, this, prefix); + Sections = BuildSections(V2NavigationItems); + BuildUrlToSectionLookup(); + } /// - /// Label-structured navigation items for V2 sidebar rendering. - /// Contains , , - /// , and existing nodes. + /// All V2 navigation items (flat list including sections, labels, etc.). + /// Used for placeholder generation and full-tree traversal. /// public IReadOnlyList V2NavigationItems { get; } + /// + /// Top-level sections extracted from . + /// Each section owns an independent sidebar nav tree. + /// + public IReadOnlyList Sections { get; } + + /// + /// Resolves which section a page belongs to by its URL. + /// Returns the first non-isolated section as fallback for unresolved URLs. + /// + public NavigationSection? GetSectionForUrl(string? pageUrl) + { + if (pageUrl is not null) + { + var normalized = pageUrl.TrimEnd('/'); + if (_urlToSection.TryGetValue(normalized, out var section)) + return section; + if (_urlToSection.TryGetValue(normalized + "/", out section)) + return section; + } + return Sections.FirstOrDefault(s => !s.Isolated); + } + + private static IReadOnlyList BuildSections(IReadOnlyList items) => + items + .OfType() + .Select(s => new NavigationSection(s.Id, s.NavigationTitle, s.Url, s.Isolated, [.. s.NavigationItems])) + .ToList(); + + private void BuildUrlToSectionLookup() + { + foreach (var section in Sections) + CollectUrls(section.NavigationItems, section); + } + + private void CollectUrls(IEnumerable items, NavigationSection section) + { + foreach (var item in items) + { + if (!string.IsNullOrEmpty(item.Url)) + { + var normalized = item.Url.TrimEnd('/'); + _ = _urlToSection.TryAdd(normalized, section); + } + + if (item is INodeNavigationItem node) + CollectUrls(node.NavigationItems, section); + } + } + private static IReadOnlyList BuildV2Items( IReadOnlyList v2Items, IReadOnlyDictionary> nodes, @@ -54,6 +113,7 @@ string sitePrefix ) => item switch { + SectionNavV2Item section => CreateSection(section, nodes, parent, sitePrefix), LabelNavV2Item label => CreateLabel(label, nodes, parent, sitePrefix), GroupNavV2Item group => CreateGroup(group, nodes, parent, sitePrefix), TocNavV2Item toc => CreateToc(toc, nodes, parent, sitePrefix), @@ -100,6 +160,18 @@ INodeNavigationItem parent public IReadOnlyCollection NavigationItems => children; } + private static SectionNavigationNode CreateSection( + SectionNavV2Item section, + IReadOnlyDictionary> nodes, + INodeNavigationItem parent, + string sitePrefix + ) + { + var placeholder = new SectionNavigationNode(section.Label, section.Url, section.Isolated, [], parent); + var children = BuildV2Items(section.Children, nodes, placeholder, sitePrefix); + return new SectionNavigationNode(section.Label, section.Url, section.Isolated, children, parent); + } + private static LabelNavigationNode CreateLabel( LabelNavV2Item label, IReadOnlyDictionary> nodes, diff --git a/src/Elastic.Documentation.Site/Layout/_SecondaryNav.cshtml b/src/Elastic.Documentation.Site/Layout/_SecondaryNav.cshtml index 84f03e90fd..736b5753db 100644 --- a/src/Elastic.Documentation.Site/Layout/_SecondaryNav.cshtml +++ b/src/Elastic.Documentation.Site/Layout/_SecondaryNav.cshtml @@ -1,3 +1,4 @@ +@using System.Linq @inherits RazorSlice
@@ -11,38 +12,61 @@ Docs
- + @if (Model.NavV2Sections is { Count: > 0 } navTabs) + { +
    + @foreach (var navTab in navTabs.Where(s => !s.Isolated)) + { + var isActive = navTab.Id == Model.ActiveSectionId; +
  • + + @navTab.Label + +
  • + } +
+ } + else + { + + } diff --git a/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs b/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs index c26a89a674..6d73feccbf 100644 --- a/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs +++ b/src/Elastic.Documentation.Site/Navigation/INavigationHtmlWriter.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using Elastic.Documentation.Navigation; +using Elastic.Documentation.Navigation.V2; using RazorSlices; namespace Elastic.Documentation.Site.Navigation; @@ -26,9 +27,15 @@ public record NavigationRenderResult public static NavigationRenderResult Empty { get; } = new() { Html = string.Empty, - Id = "empty-navigation" // random id + Id = "empty-navigation" }; public required string Html { get; init; } public required string Id { get; init; } + + /// V2 section metadata for the secondary nav bar. Null for V1 builds. + public IReadOnlyList? Sections { get; init; } + + /// The active section ID for highlighting the current tab. + public string? ActiveSectionId { get; init; } } diff --git a/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs b/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs index 57c7b6b9c9..a092e7ac17 100644 --- a/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs +++ b/src/Elastic.Documentation.Site/Navigation/NavigationViewModel.cs @@ -26,4 +26,7 @@ public class NavigationViewModel /// When true, the sidebar renders using the V2 nav partial with accordion behaviour. public bool IsNavV2 { get; init; } + + /// When true, the sidebar renders a back arrow instead of appearing in the top bar. + public bool IsIsolatedSection { get; init; } } diff --git a/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml b/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml index 5739ad37a6..bfcb6f13f3 100644 --- a/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml +++ b/src/Elastic.Documentation.Site/Navigation/_TocTree.cshtml @@ -71,6 +71,17 @@
@if (Model.IsNavV2) { + @if (Model.IsIsolatedSection) + { + + + Back to Docs + + } @* Root for V2-only CSS; JS still uses [data-nav-v2]. Scope overrides: .docs-sidebar-nav-v2 … *@
- public HashSet PrivateRepositories { get; private set; } = new(StringComparer.OrdinalIgnoreCase); + public HashSet PrivateRepositories { get; private set; } = [with(StringComparer.OrdinalIgnoreCase)]; /// /// Feature IDs that should be hidden when rendering changelog entries. /// Combined from all loaded bundles' hide-features fields. /// Entries with matching feature-id values will be excluded from the output. /// - public HashSet HideFeatures { get; private set; } = new(StringComparer.OrdinalIgnoreCase); + public HashSet HideFeatures { get; private set; } = [with(StringComparer.OrdinalIgnoreCase)]; /// /// How to handle PR/issue links relative to private bundle repos (see :link-visibility: option). diff --git a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs b/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs index a8c17ced35..4c866be716 100644 --- a/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs +++ b/src/services/Elastic.Changelog/Configuration/ChangelogConfigurationLoader.cs @@ -749,7 +749,7 @@ private static PivotConfiguration ConvertPivot(PivotConfigurationYaml yamlPivot) Dictionary? byProduct = null; if (yaml.Products is { Count: > 0 }) { - byProduct = new Dictionary(StringComparer.OrdinalIgnoreCase); + byProduct = [with(StringComparer.OrdinalIgnoreCase)]; foreach (var (productKey, productYaml) in yaml.Products) { var productIds = productKey.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); @@ -947,7 +947,7 @@ private static PivotConfiguration ConvertPivot(PivotConfigurationYaml yamlPivot) Dictionary? byProduct = null; if (yaml.Products is { Count: > 0 }) { - byProduct = new Dictionary(StringComparer.OrdinalIgnoreCase); + byProduct = [with(StringComparer.OrdinalIgnoreCase)]; foreach (var (productKey, productYaml) in yaml.Products) { var productIds = productKey.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); @@ -1012,7 +1012,7 @@ private static PivotConfiguration ConvertPivot(PivotConfigurationYaml yamlPivot) Dictionary? byProduct = null; if (yaml.Products is { Count: > 0 }) { - byProduct = new Dictionary(StringComparer.OrdinalIgnoreCase); + byProduct = [with(StringComparer.OrdinalIgnoreCase)]; foreach (var (productKey, productYaml) in yaml.Products) { var productIds = productKey.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); diff --git a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs index 04bebb7703..45cb7deaee 100644 --- a/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs +++ b/tests/Elastic.Documentation.Build.Tests/MockEnvironmentVariables.cs @@ -13,7 +13,7 @@ namespace Elastic.Documentation.Build.Tests; /// public class MockEnvironmentVariables : IEnvironmentVariables { - private readonly Dictionary _variables = new(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary _variables = [with(StringComparer.OrdinalIgnoreCase)]; /// /// Sets an environment variable value for testing. From c1f2aebfda9b8c5c0d2fa41469fe57bd1b0c2353 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 18 May 2026 17:30:24 +0200 Subject: [PATCH 27/33] chore(nav-v2): spruce up navigation labels Port the core navigation wording cleanup from PR #3326 while preserving the nav-v2-sections structure. Co-Authored-By: GPT-5.5 Co-authored-by: Cursor --- config/navigation-v2.yml | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/config/navigation-v2.yml b/config/navigation-v2.yml index 730206c69c..966c24d6e0 100644 --- a/config/navigation-v2.yml +++ b/config/navigation-v2.yml @@ -12,7 +12,7 @@ nav: - group: Elasticsearch concepts children: - page: docs-content://manage-data/data-store.md - title: Elasticsearch essentials (Core document database) + title: Elasticsearch essentials - page: docs-content://manage-data/ingest.md title: Ingest data into Elasticsearch - page: docs-content://solutions/search.md @@ -26,7 +26,7 @@ nav: - page: docs-content://solutions/search/site-or-app.md title: Add search to your site or app - page: docs-content://solutions/search/rag.md - title: RAG + title: Retrieval-augmented generation (RAG) - label: Install, deploy, and administer children: - group: Distributed architecture @@ -81,9 +81,9 @@ nav: page: docs-content://deploy-manage/reference-architectures.md children: - page: docs-content://deploy-manage/reference-architectures/hotfrozen-high-availability.md - title: Hot/Frozen - High Availability + title: Hot-frozen high availability - page: docs-content://deploy-manage/reference-architectures/genai-search-high-availability.md - title: GenAI Search - High Availability + title: GenAI search high availability - group: Production guidance page: docs-content://deploy-manage/production-guidance.md children: @@ -1647,10 +1647,10 @@ nav: children: - label: Ingest and manage data children: - - group: "Ingest or migrate: bring your data into Elasticsearch" + - group: Ingest or migrate data children: - page: docs-content://manage-data/ingest.md - title: Choose/Plan your ingest method + title: Choose an ingest method - group: Ingest architectures page: docs-content://manage-data/ingest/ingest-reference-architectures.md children: @@ -1697,7 +1697,7 @@ nav: title: Ingesting data for search use cases - title: Ingesting data for observability - page: docs-content://solutions/security/get-started/ingest-data-to-elastic-security.md - title: Ingesting data for security (Move from Solutions) + title: Ingesting data for security - page: docs-content://manage-data/ingest/ingesting-timeseries-data.md title: Ingesting time series data - group: Ingest logs @@ -2051,7 +2051,7 @@ nav: title: Index mapping and text analysis - page: docs-content://manage-data/ingest/transform-enrich/ingest-lag.md title: Calculate ingest lag metadata - - label: Search, visualize and analyze + - label: Search, visualize, and analyze children: # ======================================================================= # SEARCH AND QUERY @@ -2685,7 +2685,7 @@ nav: title: Workflow templates - label: AI and machine learning children: - - group: Machine Learning and NLP + - group: Machine learning and NLP page: docs-content://explore-analyze/machine-learning.md children: - page: docs-content://explore-analyze/machine-learning/setting-up-machine-learning.md @@ -3036,7 +3036,7 @@ nav: - page: docs-content://solutions/observability/get-started/opentelemetry/quickstart/self-managed/k8s.md title: Kubernetes - page: docs-content://solutions/observability/get-started/opentelemetry/quickstart/self-managed/hosts_vms.md - title: Hosts / VMs + title: Hosts and VMs - page: docs-content://solutions/observability/get-started/opentelemetry/quickstart/self-managed/docker.md title: Docker - group: Elastic Cloud Serverless From 37178e0b9f5b2d2b7ea20558782c15bc1c8fb0e2 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Mon, 18 May 2026 17:57:08 +0200 Subject: [PATCH 28/33] fix(nav-v2): stabilize content container layout Keep the HTMX swap target class stable so section navigation does not briefly render with the landing-page content layout before settling. Co-Authored-By: GPT-5.5 Co-authored-by: Cursor --- src/Elastic.Markdown/_Layout.cshtml | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/Elastic.Markdown/_Layout.cshtml b/src/Elastic.Markdown/_Layout.cshtml index 398141bfbd..c2b53fc0cd 100644 --- a/src/Elastic.Markdown/_Layout.cshtml +++ b/src/Elastic.Markdown/_Layout.cshtml @@ -31,19 +31,21 @@ ">
@await RenderPartialAsync(_PagesNav.Create(Model)) -
- @await RenderPartialAsync(_TableOfContents.Create(Model)) - @{ - var breadcrumbs = Model.Breadcrumbs.ToList(); - var breadcrumbVisible = breadcrumbs.Count > 0 && (breadcrumbs.Count > 1 || Model.BuildType != BuildType.Isolated); - } -
- @await RenderPartialAsync(_Breadcrumbs.Create(Model)) -
- - @await RenderBodyAsync() -
- @await RenderPartialAsync(_PrevNextNav.Create(Model)) +
+
+ @await RenderPartialAsync(_TableOfContents.Create(Model)) + @{ + var breadcrumbs = Model.Breadcrumbs.ToList(); + var breadcrumbVisible = breadcrumbs.Count > 0 && (breadcrumbs.Count > 1 || Model.BuildType != BuildType.Isolated); + } +
+ @await RenderPartialAsync(_Breadcrumbs.Create(Model)) +
+ + @await RenderBodyAsync() +
+ @await RenderPartialAsync(_PrevNextNav.Create(Model)) +
From 630626e98752ac9009f3046e5e774bf2d3beb688 Mon Sep 17 00:00:00 2001 From: Jan Calanog Date: Mon, 18 May 2026 18:12:16 +0200 Subject: [PATCH 29/33] Layout: move main content before nav in DOM source order (#3344) Scrapers and AI crawlers reading /reference pages hit a wall of navigation HTML before reaching the article. Put
first in DOM order across all three layouts while preserving the visual layout via CSS Grid order utilities. Co-authored-by: Claude Opus 4.7 --- src/Elastic.ApiExplorer/_Layout.cshtml | 4 ++-- src/Elastic.Codex/_MarkdownLayout.cshtml | 8 ++++---- src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml | 2 +- src/Elastic.Markdown/Layout/_TableOfContents.cshtml | 2 +- src/Elastic.Markdown/_Layout.cshtml | 8 ++++---- 5 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Elastic.ApiExplorer/_Layout.cshtml b/src/Elastic.ApiExplorer/_Layout.cshtml index d8dd9ab92f..3a39175ea7 100644 --- a/src/Elastic.ApiExplorer/_Layout.cshtml +++ b/src/Elastic.ApiExplorer/_Layout.cshtml @@ -16,8 +16,7 @@ else grid-cols-1 md:grid-cols-[var(--max-sidebar-width)_1fr] "> - @await RenderPartialAsync(_PagesNav.Create(Model)) -
+
@@ -28,6 +27,7 @@ else
@await RenderPartialAsync(_ApiToc.Create(Model.TocItems.ToArray()))
+ @await RenderPartialAsync(_PagesNav.Create(Model))
@if (Model.BuildType == BuildType.Assembler) diff --git a/src/Elastic.Codex/_MarkdownLayout.cshtml b/src/Elastic.Codex/_MarkdownLayout.cshtml index 9cbc00e844..d980c42e70 100644 --- a/src/Elastic.Codex/_MarkdownLayout.cshtml +++ b/src/Elastic.Codex/_MarkdownLayout.cshtml @@ -25,10 +25,8 @@ md:px-4 ">
- @await RenderPartialAsync(_PagesNav.Create(Model)) -
- @await RenderPartialAsync(_TableOfContents.Create(Model)) -
+
+
@await RenderPartialAsync(_Breadcrumbs.Create(Model))
@@ -36,7 +34,9 @@
@await RenderPartialAsync(_PrevNextNav.Create(Model))
+ @await RenderPartialAsync(_TableOfContents.Create(Model))
+ @await RenderPartialAsync(_PagesNav.Create(Model))
diff --git a/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml b/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml index d04673c553..2a020db1b0 100644 --- a/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml +++ b/src/Elastic.Documentation.Site/Layout/_PagesNav.cshtml @@ -1,5 +1,5 @@ @inherits RazorSlice -