Skip to content

Prototype nav-v2: new IA with label sections, accordion sidebar#2927

Draft
theletterf wants to merge 159 commits into
mainfrom
nav-v2
Draft

Prototype nav-v2: new IA with label sections, accordion sidebar#2927
theletterf wants to merge 159 commits into
mainfrom
nav-v2

Conversation

@theletterf
Copy link
Copy Markdown
Member

@theletterf theletterf commented Mar 20, 2026

Prototypes a new information architecture sidebar gated behind the nav-v2 feature flag (auto-enabled in dev and preview environments). Content is served at the same URL paths as V1 — only the sidebar layout changes.

Demo: https://docs-v3-preview.elastic.dev/elastic/docs-builder/docs/2927/get-started

Contributes to: https://github.com/elastic/docs-content-internal/issues/1015


What's included

This branch now incorporates the work from PR #3133 (nav-v2-sections, merged into nav-v2), which adds independent sidebar islands and a dynamic data-driven top bar on top of the base nav-v2 prototype.

From this PR (nav-v2 base)

  • New navigation-v2.yml file drives the sidebar, separate from navigation.yml.
  • Five item types: label:, toc:, page:, group:, title:.
  • Placeholder stub pages — every title: leaf and group: folder without a page: gets a generated stub at /{sitePrefix}/_placeholder/{hash}/.
  • Accordion sidebar behaviour, visual hierarchy, and expand/collapse logic.

From PR #3133 (nav-v2-sections, merged)

  • section: item type: each section owns its own sidebar HTML — pages in one section never include another section's nav links in the DOM.
  • island: item type: intra-section subtrees that get their own focused sidebar when entered, with a back arrow pointing to the nearest ancestor page. Island detection uses toc root identity, not URL matching.
  • Dynamic secondary nav bar: data-driven from the nav-v2 file instead of hardcoded. Sections appear as left-aligned compact tabs. Isolated sections (isolated: true) do not appear in the top bar.
  • Per-section sidebar caching in GlobalNavigationHtmlWriter — each section's sidebar is rendered and cached independently.
  • External URLs in section url: fields open in a new tab with a ↗ indicator.
  • Section root pages skip current-page highlighting and auto-expand top-level folders for discoverability.

Tab ordering

Guides | APIs ↗ | Reference | Troubleshoot | Release notes

Sections (top bar tabs)

Section Preview URL
Guides (full V2 tree) https://docs-v3-preview.elastic.dev/elastic/docs-builder/docs/2927/
APIs https://www.elastic.co/docs/api (external, opens in new tab)
Reference https://docs-v3-preview.elastic.dev/elastic/docs-builder/docs/2927/reference/
Troubleshoot https://docs-v3-preview.elastic.dev/elastic/docs-builder/docs/2927/troubleshoot/
Release notes https://docs-v3-preview.elastic.dev/elastic/docs-builder/docs/2927/release-notes/

Isolated sections (not in top bar)

Section Preview URL
Extension points https://docs-v3-preview.elastic.dev/elastic/docs-builder/docs/2927/extend/

Nav islands (intra-section, with back arrow)

Island Parent Preview URL
Account and preferences Guides https://docs-v3-preview.elastic.dev/elastic/docs-builder/docs/2927/cloud-account

Note: Logstash plugins and versioned plugins islands are configured but use logstash-docs-md (a private repo), so they are not accessible in PR previews. Test locally with dotnet run --project src/tooling/docs-builder -- assembler build && dotnet run --project src/tooling/docs-builder -- assembler servehttp://localhost:4000/docs/reference/logstash/plugins/


Navigation YAML format (config/navigation-v2.yml)

nav:
  - section: Guides            # top bar tab, owns full sidebar
    url: /
    children:
      - label: Elasticsearch fundamentals
        children: [...]
      - island: Account and preferences   # focused sidebar + back arrow
        toc: docs-content://cloud-account

  - section: APIs              # external link, new tab with ↗
    url: https://www.elastic.co/docs/api

  - section: Reference         # top bar tab, own sidebar
    url: /reference/
    children:
      - toc: docs-content://reference/security
      - island: Logstash plugins          # focused sidebar + back arrow
        toc: logstash-docs-md://lsr

  - section: Troubleshoot      # top bar tab, own sidebar
    url: /troubleshoot/
    children:
      - toc: docs-content://troubleshoot

  - section: Release notes     # top bar tab, own sidebar
    url: /release-notes/
    children:
      - toc: docs-content://release-notes/intro

  - section: Extension points  # isolated: not in top bar
    url: /extend/
    isolated: true
    children:
      - toc: docs-content://extend

The remaining item types (label:, toc:, page:, group:, title:) behave as documented below.


Placeholder stub pages

Every title: leaf and group: folder that lacks a page: gets a generated stub page at /{sitePrefix}/_placeholder/{hash}/. The stub renders the full site chrome with an <h1> of the placeholder title and a "This content is coming soon." paragraph. All placeholder nav items are greyed but clickable.


Filling the gaps

  • Replace title: Foo with toc: repo://path when a whole TOC root covers the topic.
  • Replace title: Foo with page: docs-content://path/to/file.md when a specific existing page covers the topic.
  • Replace group: Foo with toc: when the folder gains a real root, or keep it as group: with children if the folder itself has no canonical URL.

Sidebar behaviour

  • Labels (any depth): always expanded, no toggle, no chevron.
  • group: nodes: expand/collapse toggle; accordion at top level; greyed link → stub page.
  • toc: nodes: full live tree, same expand/collapse as V1.
  • page: leaves: clickable link to the canonical page URL.
  • title: leaves: greyed link → stub page.

URL scheme — known limitation and future direction

toc: entries currently use the same URL paths as V1 (navigation.yml), because the file-writing pipeline derives output paths from NavigationTocMappings, which is populated from navigation.yml before SiteNavigationV2 is constructed. The intended final scheme ({scheme}/{host}/{path}) requires wiring the V2 nav file into AssembleSources.AssembleAsync. That work is out of scope for this PR.

🤖 Generated with Claude Code

theletterf and others added 9 commits March 20, 2026 09:37
Adds a new `navigation-v2.yml`-driven sidebar behind the `nav-v2`
feature flag (enabled by default in dev/preview environments).

- New YAML format: label sections (non-clickable headings), toc entries
  that resolve to existing navigation nodes at their original URL paths,
  page crosslinks, and title-only placeholder (disabled) links
- `SiteNavigationV2` extends `SiteNavigation`, passes the original
  nav file to the base constructor so content URLs are unchanged; builds
  a separate `V2NavigationItems` tree for sidebar rendering
- `GlobalNavigationHtmlWriter` detects `SiteNavigationV2` and returns
  the same full V2 nav HTML (cached once) for every page
- `_TocTreeNavV2.cshtml` renders labels as `<span>`, placeholders as
  `aria-disabled` anchors, folders/leaves same as V1
- `pages-nav-v2.ts` adds accordion collapse (open one section → others
  collapse) and current-page marking with no auto-expand
- Feature flag key normalisation: assembler.yml uses `NAV_V2` (underscore)
  but lookup uses `nav-v2` (hyphen); fixed by calling `featureFlags.Set`
  which normalises via `ToLowerInvariant().Replace('_', '-')`

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
ShortId.Create("label") always produced the same SHA256 hash, so all
8 label checkboxes shared id="v2-label-1ACA80E8". Every <label for="">
targeted the first checkbox, making only "Get Started" expandable.

Fix: include the label text in the hash — ShortId.Create("label", label).

Also updates nav-v2-status.md to reflect verified accordion behaviour.

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

Translates the team's proposed information architecture into the V2 nav:

Top-level labels:
  • Elasticsearch fundamentals (get-started + placeholder concepts)
  • Install, deploy, and administer (deploy-manage + cloud-account)
  • The Elasticsearch Platform (container for nested labels)
  • Solutions and project types (solutions)
  • Reference (elasticsearch + kibana)
  • Troubleshooting (troubleshoot)

"The Elasticsearch Platform" has three nested labels:
  • Ingest and manage data → toc: manage-data
  • Search, visualize, and analyze → toc: explore-analyze
  • AI and machine learning → title: placeholders (content lives in
    explore-analyze today; dedicated toc roots needed to wire up)

Nested labels work with no code changes — the YAML parser, SiteNavigationV2
builder, and _TocTreeNavV2 partial already recurse through children at any
depth. The Razor partial applies level-aware styling (font-semibold at
level 0, lighter weight at depth 1+).

Also documents the proposed IA mapping and content split analysis
in nav-v2-status.md.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Labels at any depth are now unconditionally expanded — no checkbox,
no chevron, children always visible. This applies to both top-level
labels (Elasticsearch fundamentals, The Elasticsearch Platform, etc.)
and nested labels (Ingest and manage data, Search/visualize, AI/ML).

_TocTreeNavV2.cshtml: for LabelNavigationNode, removed the peer div /
checkbox / chevron pattern; render a plain <span> heading followed by
an always-visible <ul> of children. Removed data-v2-accordion from
label <li> elements since labels no longer participate in accordion.

TOC folder nodes (INodeNavigationItem) retain their existing
checkbox-driven expand/collapse toggle unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Verified behaviours: labels always expanded (no toggle), nested
  labels same, placeholders render as disabled, TOC folders still
  toggle, accordion scoped to TOC siblings
- Mark nested label support as done
- Correct build/serve commands to dotnet run invocations
- Note LabelNavigationNode.ExpandedByDefault is now unused

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Level-1 labels (top-level sections): font-semibold text-sm text-ink — bold,
14px, full ink colour.

Level-2+ labels (sub-section dividers): text-[10px] font-semibold uppercase
tracking-widest text-ink/50 — all-caps, 10px, wide letter-spacing, 50%
opacity. This is the standard sidebar sub-group treatment (VS Code, Linear,
Notion style) and makes the hierarchy immediately legible at a glance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Level-1 label text: text-xs uppercase tracking-widest font-semibold
  text-ink — same small-caps treatment as level-2 but full ink colour,
  making them clearly structural dividers not clickable links
- Level-1 label <li>: border-t border-grey-20 pt-4 mt-4 to draw a thin
  horizontal rule between each top-level section; first: variant removes
  the border/padding from the first item

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Was text-[10px] text-ink/50 — too small and too dim to read comfortably.
Now text-xs (12px) text-ink/65 — same small-caps uppercase treatment,
clearly subordinate to level-1 but readable at a glance.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/Elastic.Documentation.Site/Assets/pages-nav-v2.ts Fixed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment thread src/Elastic.Documentation.Navigation/V2/SiteNavigationV2.cs Fixed
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 20, 2026

🔍 Preview links for changed docs

Introduces `GroupNavV2Item` — a YAML `group:` item that renders as an
expandable folder with a disabled (cursor-not-allowed) link and a chevron
toggle. Unlike `label:` (always-expanded section heading), `group:` behaves
like a regular TOC folder but with no real URL.

Changes:
- `NavigationV2File.cs`: add `GroupNavV2Item` record + `group:` YAML parsing
- `PlaceholderNavigationNode.cs`: new nav node implementing INodeNavigationItem
- `SiteNavigationV2.cs`: `CreateGroup` builder (mirrors `CreateLabel`)
- `_TocTreeNavV2.cshtml`: render PlaceholderNavigationNode as disabled folder
- `navigation-v2.yml`: replace `toc: manage-data` in "Ingest and manage data"
  with the full "Ingest or migrate: bring your data into Elasticsearch" tree
  (placeholder titles only; no toc/page refs yet)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Translates the complete card-sort JSON into navigation-v2.yml using
group:/title: placeholders throughout. All 6 top-level labels are
populated with the full proposed IA structure — no toc/page refs yet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Switch NavV2Deserializer from DeserializerBuilder to
  StaticDeserializerBuilder — our NavV2FileYamlConverter parses
  manually via parser events so the static context is sufficient;
  fixes IL3050/IL2055 AOT errors in the native build
- Remove unused `$` import from pages-nav-v2.ts; fixes ESLint
  @typescript-eslint/no-unused-vars error in the npm check

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
theletterf and others added 27 commits May 18, 2026 14:52
Keep the preview header guard from breaking the frontend Prettier check on the PR merge result.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
After merging main, IDE0028 is enforced across the merged branch, so use the repo's collection expression syntax where the analyzer now requires it.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve the content date enrichment JsonArray conflict by keeping the explicit JsonNode cast from nav-v2.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Port the core navigation wording cleanup from PR #3326 so the base nav branch carries the latest IA label refinements.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Port the core navigation wording cleanup from PR #3326 while preserving the nav-v2-sections structure.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the nav-v2-sections navigation structure after applying the shared PR #3326 label refinements.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
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 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the single aggregate docs-content://reference TOC with
explicit per-subdirectory entries so each reference area loads
independently without duplicating top-level sections in the sidebar.
Logstash plugin entries remain as nav islands with back-arrow.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Replace the single aggregate docs-content://reference TOC with
explicit per-subdirectory entries so each reference area loads
independently without duplicating top-level sections in the sidebar.
Logstash plugin entries added as nav islands with back-arrow.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Back arrow: instead of always pointing at the parent *section* URL
(which is "/" for the Guides section), walk up the IslandNavigationNode
parent chain to the nearest ancestor that has a real URL. Store it as
BackUrl on NavigationIsland and use it in GlobalNavigationHtmlWriter.

Parent nav: render island entries as plain links with no expandable
subtree. The island's own sidebar (loaded via HTMX full-swap) already
shows the full tree; expanding it inline was duplicate and confusing.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Accept main's DOM reorder (nav after </main>, md:order-2 on main,
order-2 lg:order-1 on article) while keeping the stable min-w-0
class on #content-container and wrapping the grid CSS in an inner
<div> to prevent the HTMX-swap layout jump.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Accept main's DOM reorder (nav after </main>, md:order-2 on main,
order-2 lg:order-1 on article) while keeping the stable min-w-0
class on #content-container and wrapping the grid CSS in an inner
<div> to prevent the HTMX-swap layout jump.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Take section: Release notes (with url: /release-notes/) from
nav-v2-sections over the plain label: entry in nav-v2, preserving
the sections feature this PR introduces.

Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Resolve PR #2927 mergeability by updating nav-v2 with the latest main
changes so the branch merges cleanly without conflict.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Register section root URLs as authoritative owners and normalize site-prefixed lookups so section landing pages keep their own sidebar. Make island back links preserve the active site prefix, including nested reference islands such as Logstash plugins.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Keep the query language reference TOC as the single sidebar entry so the narrative pages no longer duplicate the reference structure.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Only auto-open a section root sidebar when it has a single top-level folder, avoiding the fully expanded Reference nav while preserving single-TOC sections.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
* Keep nav-v2 focused on the active section

Same-section navigation could leave stale folders expanded, which made the sidebar show multiple open branches. Recomputing expansion from the active page path keeps one relevant branch open and preserves accordion behavior when folders are opened manually.

Co-Authored-By: OpenAI GPT-5.4 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Auto-scroll nav-v2 to the active branch

Opening a lower section could still leave part of the active branch clipped below the sidebar viewport. Re-scrolling after the nav layout settles keeps the selected branch fully visible and locks in the behavior with a focused regression test.

Co-Authored-By: OpenAI GPT-5.4 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Re-scroll nav-v2 after sidebar chrome resizes

The Jump to page web component can alter the sidebar chrome after the active branch scroll is first calculated. Observing the sidebar chrome and scroll container makes the active branch scroll correction respond to those late layout changes.

Co-Authored-By: OpenAI GPT-5.4 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

* Format nav-v2 scroll observer

Prettier wrapped the ResizeObserver map type so the npm formatting check passes in CI.

Co-Authored-By: OpenAI GPT-5.4 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: OpenAI GPT-5.4 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Bring nav-v2 up to date with main so shared unit test fixes are included before continuing prototype work.

Co-Authored-By: OpenAI GPT-5.4 <noreply@openai.com>
Inline callouts can contain Markdown links, but the parser rejected slash characters and skipped those callouts. Allow slashes in inline callout text and render inline callout Markdown without splitting the list item body so the existing formatting expectation passes.

Co-Authored-By: OpenAI GPT-5.4 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Allow selected Nav V2 folders to stay open by default without losing manual collapse behavior, so high-priority guide sections remain visible while deeper folders stay collapsed.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Constrain the page grid and mobile nav chrome so wide header content does not force the docs viewport to scroll horizontally.

Co-Authored-By: GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Store the clicked sidebar occurrence so duplicate page links keep the user's chosen nav context, while direct loads use a stable canonical fallback.

Co-Authored-By: OpenAI GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
@theletterf
Copy link
Copy Markdown
Member Author

Added a temporary workaround for duplicate Nav V2 page entries.

Some pages intentionally appear in more than one sidebar location while the IA is still being validated. Instead of removing those duplicates now, the sidebar now assigns each rendered nav link a stable data-nav-v2-location, remembers the location the user clicked for that page in sessionStorage, and focuses only that selected occurrence after navigation. Direct loads, organic search, shared URLs, and reloads still use the same canonical page URL and fall back to the first stable nav occurrence, with the existing deeper same-branch behavior preserved for folder/index duplicates.

This keeps one page URL and avoids duplicate search results, while preventing multiple duplicate sidebar entries from being highlighted or expanded at the same time. We can remove or explicitly canonicalize duplicate nav entries later as a separate IA decision.

theletterf and others added 2 commits May 20, 2026 13:57
Co-Authored-By: OpenAI GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Only treat same-page folder-row clicks as expand toggles when that exact row is already focused, so duplicate rows can still update the selected nav location.

Co-Authored-By: OpenAI GPT-5.5 <noreply@openai.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

10 participants