Skip to content

Navigation: level-0 section: entries drive the top nav bar - #3792

Open
theletterf wants to merge 7 commits into
mainfrom
feature/configurable-top-nav
Open

Navigation: level-0 section: entries drive the top nav bar#3792
theletterf wants to merge 7 commits into
mainfrom
feature/configurable-top-nav

Conversation

@theletterf

@theletterf theletterf commented Aug 6, 2026

Copy link
Copy Markdown
Member

Summary

Adds configurable top-nav driven by section: entries in config/navigation_preview.yml.
The 11 flat toc: entries are grouped into 5 named tabs without moving any pages.

Design: config-level grouping

section: entries are YAML-only — they are not tree nodes. Their toc: children are added
to SiteNavigation.TopLevelItems as flat roots, exactly as before. Active state is resolved
by matching the current page's NavigationRoot.Id against the section's set of child IDs.

No page URLs change. FileNavigationLeaf.DetermineUrl derives URLs from each entry's own
path_prefix, which is positional-independent. URL invariance is the primary correctness gate.

Config surface (navigation_preview.yml)

Two entry shapes are supported:

toc:
  - section: Guides          # groups multiple toc: roots under one tab
    children:
      - toc: get-started
      - toc: solutions
      ...

  - section: APIs            # external link — external: <url>
    external: https://www.elastic.co/docs/api/

dropdown: is deferred until hub pages land in docs-content.

What stays unchanged

  • All page URLs and path prefixes
  • LlmsNavigationEnhancer and sitemap logic
  • Breadcrumb computation
  • navigation.yml (flat, for non-preview builds)

What's new

File Change
config/navigation_preview.yml 5 section: groups replace 11 flat entries
SiteNavigationFile.cs ISiteNavigationEntry, SiteSectionRef, updated YAML converters
SiteNavigation.cs iterates ISiteNavigationEntry, descends into section children
SectionTopNavBuilder.cs derives TopNavRenderModel from nav file entries
BuildContext.cs TopNav property wires render model to every page
_SecondaryNav.cshtml renders tabs (no arrow on external links)
AssembleSources.cs ReadBlock descends into section children

Not in scope (deferred)

  • dropdown: sections — waiting for hub pages to land in docs-content
  • Mobile-width tab overflow — follow-up PR

Test plan

  • dotnet build passes (0 errors)
  • dotnet test tests/Elastic.Documentation.Configuration.Tests passes (673 tests)
  • dotnet test tests/Navigation.Tests passes (216 tests)
  • npm test for secondary-nav.test.ts
  • Local preview: correct active tab on each section's pages, APIs opens in new tab

🤖 Generated with Claude Code

@theletterf

Copy link
Copy Markdown
Member Author

@florent-leborgne — the dropdown from #3223 is fully supported here, it just moved to a slightly different schema. Here is how to turn Products on when the pages are ready.

Drop this into top_nav: in config/navigation.yml, between Guides and APIs (list order is bar order):

  - title: Products
    children:
      - title: Stack products      # a child WITH children => group heading
        children:
          - title: Elasticsearch
            page: docs-content://products/elasticsearch/v9.md
          - title: Kibana
            page: docs-content://products/kibana/v9.md

That renders exactly the panel in your screenshot: a "Stack products" heading with the two links under it.

What changed from your prototype

#3223 here why
dropdown: true (nothing) the presence of children: is what makes it a dropdown, so there is no flag to forget
section: / label: / group: title: everywhere one key at every depth; position in the tree decides whether it is a tab, a heading or a link
url: on the dropdown ignored (warns) see below

The dropdown label is a pure toggle, not a link. Your version put an <a> inside <summary>, which fights the native <details> toggle on click — the first click had to both navigate and open. Here the label only opens the panel. If you set url: or page: on an entry that has children:, the build warns and ignores it rather than silently dropping the link.

So if Products needs its own /products/ landing page, add it as the first entry inside the panel:

  - title: Products
    children:
      - title: All products        # childless entries render as links with no heading
        url: /products/
      - title: Stack products
        children:
          - title: Elasticsearch
            page: docs-content://products/elasticsearch/v9.md

Childless children are collected into an unlabelled run at the position you put them, so this puts "All products" above the "Stack products" heading. If a dropdown landing page turns out to be a real requirement, say so and I will look at making the label both toggle and link properly.

Things that will fail the build

  • An unresolvable page: is an error, not a silent drop. page: refs go through the same CrossLinkResolver as body cross-links, so they survive page moves — but the top nav is on every page, so a bad ref fails the assemble rather than shipping a broken tab. I have not checked whether products/elasticsearch/v9.md and products/kibana/v9.md currently resolve on main; if they do not yet exist, that entry will need to wait or point elsewhere.
  • Nesting is capped at item → group → link. A third level inside the panel is an error, not a silently truncated render (your version just stopped drawing at depth 2).
  • Setting both url: and page: on one entry is an error.

Not ported

The - title: X placeholder that rendered a greyed-out soon badge. Here an entry with no url:, page: or children: is a config error. It is maybe 15 lines to bring back (a TopNavStubItem plus the .secondary-nav-dropdown-stub CSS, which I left out of secondary-nav-dropdown.css) — worth doing if the hub pages land before the product pages, so just ask.

Two behaviours you get for free

  • Active state. A page under any dropdown child highlights the Products label, the same way Reference highlights on /reference/*. Longest match wins, so a nested entry beats its parent.
  • Outside click and Escape close the panel. Native <details> does neither — the prototype's CSS comment claimed the browser handled outside-click, but it does not, so an open panel sat over the page until you clicked the summary again. secondary-nav.ts fixes both, and opening one dropdown closes its siblings.

@reakaleek reakaleek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't work on mobile once we add "Products".

The current items are taking up the whole width already.

image

We need a solution for the mobile view.

But really nice idea on making it configurable.

@theletterf
theletterf force-pushed the feature/configurable-top-nav branch from d701ff9 to 61c8b5b Compare August 13, 2026 15:42
@theletterf theletterf changed the title Navigation: make the top nav configurable in navigation.yml Navigation: level-0 section: entries drive the top nav bar Aug 13, 2026
Comment thread config/navigation.yml Outdated

@Mpdreamz Mpdreamz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

dropdown:

Lets not do this now, lets wait for @florent-leborgne hub pages to land in docs-builder and docs-content and then think about how we want to expose this.

external:

can that be external: url, that way we can include API's to bump.sh for now.

…w.yml

Adds a config-level section: grouping to navigation_preview.yml that maps
the 11 flat top-level toc: entries to 5 named tabs in the top nav bar.
Sections are config-only — children are added to the site tree as flat
roots, so no page URLs change.

New ISiteNavigationEntry interface and SiteSectionRef record allow the
YAML converter to parse both toc: and section: entries. SiteTableOfContents
becomes List<ISiteNavigationEntry>. SiteNavigation iterates entries and
descends into section children when encountered.

SectionTopNavBuilder derives TopNavRenderModel from the nav file's entry
list. Section tabs carry SectionIds so active state is resolved by
comparing the current page's NavigationRoot.Id to the tab's set.

BuildContext.TopNav wires the render model through to every page.
_SecondaryNav.cshtml renders the tabs and delegates dropdown/external
behaviour to secondary-nav.ts.

Section structure in navigation_preview.yml:
  - section: Guides (get-started, solutions, manage-data, explore-analyze,
                     deploy-manage, cloud-account)
  - section: Troubleshoot (troubleshoot)
  - section: Release notes (release-notes/intro + release children)
  - section: Reference (reference + reference children)
  - section: Extend (extend, contribute-docs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@theletterf theletterf left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review, Martijn!

On dropdown:: Agreed — deferring until hub pages land in docs-content. The implementation already removes the dropdown: surface from this PR. We can add it back in a follow-up once we have a concrete content target.

On external: url syntax: The current external: true + url: split is intentional. Here is why:

external: is a behavioral flag (open in new tab, no active-state tracking). url: is the destination. Keeping them separate gives us:

  1. Consistency with internal sections: Internal sections can also carry a url: override (tab points at a specific page instead of the first child's index). A single external: <url> key would make external and internal sections use different mechanisms for the same concept.
  2. Validation surface: With two keys, the YAML validator can catch external: true without url: (ambiguous) or url: without external: true (assumed internal) independently. With external: <url>, the value of the key carries dual meaning — both "is external" and "what URL" — and the code must distinguish external: true from external: https://....
  3. Extensibility: If we later need rel: nofollow or target: _blank options, they sit naturally alongside external: true. With external: url, those attributes have no obvious home.

That said, if you feel strongly about the syntax change I can implement external: <url> — it would be a small change to the YAML converter. Let me know.

Separately: the conflicts with main (navigation_preview.yml feature-flag mechanism, Island = false on SiteTableOfContentsRef, new CheckoutsFileSystem in AssemblerBuildService) are now resolved. The branch is rebased cleanly on top of the commits you merged this week.

…row icon

Replace two-key syntax (external: true + url:) with single-key:
  - section: APIs
    external: https://...

SiteSectionRef now carries ExternalUrl (string?) with IsExternal computed.
_SecondaryNav.cshtml drops the SVG arrow from external link tabs.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@theletterf theletterf left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

External sections now use external: <url> directly, no separate url: key needed. The arrow icon is also removed from external tabs.

- section: APIs
  external: https://www.elastic.co/docs/api/

SiteSectionRef carries ExternalUrl (string?) with IsExternal computed from it.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…hetics

The sidebar dropdown (isUsingNavigationDropdown) is suppressed when
section tabs are active. Navigate to Reference via #secondary-nav instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Links to https://www.elastic.co/docs/api/ using the external: <url> syntax.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…oot, Release notes, Extend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@theletterf

Copy link
Copy Markdown
Member Author

@copilot Push an empty commit to retrigger CI.

Copilot AI commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

@copilot Push an empty commit to retrigger CI.

Done — pushed to retrigger CI.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants