diff --git a/_includes/sidebar.html b/_includes/sidebar.html
deleted file mode 100644
index 3334ec8a057..00000000000
--- a/_includes/sidebar.html
+++ /dev/null
@@ -1,15 +0,0 @@
-{% assign sidebar = site.data.sidebars[page.sidebar].entries %}
-
-
diff --git a/_includes/sidebar_recursive.html b/_includes/sidebar_recursive.html
deleted file mode 100644
index 9a3870d487c..00000000000
--- a/_includes/sidebar_recursive.html
+++ /dev/null
@@ -1,122 +0,0 @@
-{% comment %}
- Recursive sidebar: renders nested doc nav from _data/sidebars via folder.folderitems.
- Parameters: item (folder node), level (1–4), parent_id (element id for data-bs-parent accordion group).
- Branch rows use BS5 Collapse (data-bs-toggle, .collapse.show); leaves are plain nav-links.
- Active state compares page.url (no leading slash) to each item url.
-{% endcomment %}
-
-{% assign page_url_clean = page.url | remove_first: "/" %}
-
-{% assign item = include.item %}
-{% assign title = item.title %}
-{% assign url = item.url %}
-{% assign folderitems = item.folderitems %}
-{% assign subfolders = item.subfolders %}
-{% assign subfolderitems = item.subfolderitems %}
-
-{% assign is_branch = false %}
-{% if include.level == 1 %}
- {% if folderitems.size > 0 %}
- {% assign is_branch = true %}
- {% endif %}
-{% elsif include.level == 3 %}
- {% assign is_branch = true %}
-{% endif %}
-
-{% if is_branch == false %}
- {% assign current_url_clean = url | remove_first: "/" %}
-
-
- {{ title }}
-
-
-{% else %}
- {% capture current_uid %}{{ include.level }}-{{ title | slugify }}{% endcapture %}
- {% assign current_id = current_uid | prepend: "sb-" %}
-
- {% assign any_active = false %}
- {% assign current_url_clean = url | remove_first: "/" %}
- {% if page_url_clean == current_url_clean and current_url_clean != "" %}{% assign any_active = true %}{% endif %}
-
- {% if any_active == false %}
- {% for fi in folderitems %}
- {% assign fi_url = fi.url | remove_first: "/" %}
- {% if page_url_clean == fi_url and fi_url != "" %}{% assign any_active = true %}{% break %}{% endif %}
- {% for sf in fi.subfolders %}
- {% for sfi in sf.subfolderitems %}
- {% assign sfi_url = sfi.url | remove_first: "/" %}
- {% if page_url_clean == sfi_url and sfi_url != "" %}{% assign any_active = true %}{% break %}{% endif %}
- {% endfor %}
- {% if any_active %}{% break %}{% endif %}
- {% endfor %}
- {% if any_active %}{% break %}{% endif %}
- {% endfor %}
- {% endif %}
-
- {% if any_active == false %}
- {% for sf in subfolders %}
- {% for sfi in sf.subfolderitems %}
- {% assign sfi_url = sfi.url | remove_first: "/" %}
- {% if page_url_clean == sfi_url and sfi_url != "" %}{% assign any_active = true %}{% break %}{% endif %}
- {% endfor %}
- {% if any_active %}{% break %}{% endif %}
- {% endfor %}
- {% endif %}
-
- {% if any_active == false %}
- {% for sfi in subfolderitems %}
- {% assign sfi_url = sfi.url | remove_first: "/" %}
- {% if page_url_clean == sfi_url and sfi_url != "" %}{% assign any_active = true %}{% break %}{% endif %}
- {% endfor %}
- {% endif %}
-
-
-
- {{ title }}
-
-
-
-{% endif %}
diff --git a/layouts/partials/sidebar.html b/layouts/partials/sidebar.html
new file mode 100644
index 00000000000..c6743085b7f
--- /dev/null
+++ b/layouts/partials/sidebar.html
@@ -0,0 +1,33 @@
+{{- /* Renders the sidebar navigation. Reads the sidebar key from page
+ params, looks up the matching data file in data/sidebars/, and
+ delegates tree rendering to sidebar_recursive.html. */ -}}
+{{- $sidebarKey := .Param "sidebar" | default "home_sidebar" -}}
+{{- $sidebarKeyText := strings.TrimSpace (printf "%v" $sidebarKey) -}}
+{{- $hasNoSidebar := or (eq $sidebarKeyText "nil") (eq $sidebarKeyText "") (eq $sidebarKeyText "false") -}}
+{{- if not $hasNoSidebar -}}
+{{- $sidebarData := index hugo.Data.sidebars $sidebarKey -}}
+{{- $entries := slice -}}
+{{- with $sidebarData.entries }}{{ $entries = . }}{{ end -}}
+{{- $firstEntry := index $entries 0 -}}
+
+{{- if $firstEntry -}}
+ {{- $product := $firstEntry.product | default $firstEntry.title | default "" -}}
+ {{- $version := $firstEntry.version | default "" -}}
+
+{{- else -}}
+ {{- with $sidebarKey }}
+
+ {{- end }}
+{{- end -}}
+{{- end -}}
diff --git a/layouts/partials/sidebar_is_web.html b/layouts/partials/sidebar_is_web.html
new file mode 100644
index 00000000000..ddf592e6822
--- /dev/null
+++ b/layouts/partials/sidebar_is_web.html
@@ -0,0 +1,5 @@
+{{- /* Returns truthy if the item should appear in web output
+ (i.e. its "output" field contains "web", defaulting to yes). */ -}}
+{{- $output := "web" -}}
+{{- with .output }}{{ $output = printf "%v" . }}{{ end -}}
+{{- if in $output "web" -}}true{{- end -}}
diff --git a/layouts/partials/sidebar_item_active.html b/layouts/partials/sidebar_item_active.html
new file mode 100644
index 00000000000..fc7e479c899
--- /dev/null
+++ b/layouts/partials/sidebar_item_active.html
@@ -0,0 +1,43 @@
+{{- /* Recursively checks if any item in the given subtree matches the
+ current page. Returns truthy when a match is found, used to
+ expand parent collapse sections. Params: item, current. */ -}}
+{{- $item := .item -}}
+{{- $current := .current -}}
+{{- $active := false -}}
+
+{{- with $item.url }}
+ {{- if partial "sidebar_page_matches_url.html" (dict "page" $current "url" .) }}
+ {{- $active = true -}}
+ {{- end }}
+{{- end -}}
+
+{{- if not $active }}
+ {{- range $child := $item.folderitems }}
+ {{- if partial "sidebar_item_active.html" (dict "item" $child "current" $current) }}
+ {{- $active = true -}}
+ {{- end }}
+ {{- range $subfolder := $child.subfolders }}
+ {{- if partial "sidebar_item_active.html" (dict "item" $subfolder "current" $current) }}
+ {{- $active = true -}}
+ {{- end }}
+ {{- end }}
+ {{- end }}
+{{- end -}}
+
+{{- if not $active }}
+ {{- range $subfolder := $item.subfolders }}
+ {{- if partial "sidebar_item_active.html" (dict "item" $subfolder "current" $current) }}
+ {{- $active = true -}}
+ {{- end }}
+ {{- end }}
+{{- end -}}
+
+{{- if not $active }}
+ {{- range $child := $item.subfolderitems }}
+ {{- if partial "sidebar_item_active.html" (dict "item" $child "current" $current) }}
+ {{- $active = true -}}
+ {{- end }}
+ {{- end }}
+{{- end -}}
+
+{{- if $active -}}true{{- end -}}
diff --git a/layouts/partials/sidebar_page_matches_url.html b/layouts/partials/sidebar_page_matches_url.html
new file mode 100644
index 00000000000..7072fe3a4ae
--- /dev/null
+++ b/layouts/partials/sidebar_page_matches_url.html
@@ -0,0 +1,20 @@
+{{- /* Checks if a page matches a target URL. Compares against the page's
+ RelPermalink, front-matter url, permalink, and aliases for a
+ reliable match regardless of how the URL was defined. */ -}}
+{{- $page := .page -}}
+{{- $target := partial "sidebar_url_clean.html" .url -}}
+{{- $matches := false -}}
+{{- if and $page $target }}
+ {{- $urls := slice -}}
+ {{- with $page.RelPermalink }}{{ $urls = $urls | append . }}{{ end -}}
+ {{- with $page.Params.url }}{{ $urls = $urls | append . }}{{ end -}}
+ {{- with $page.Params.permalink }}{{ $urls = $urls | append . }}{{ end -}}
+ {{- with $page.Params.aliases }}
+ {{- range . }}{{ $urls = $urls | append . }}{{ end -}}
+ {{- end -}}
+ {{- range $urls }}
+ {{- $clean := partial "sidebar_url_clean.html" . -}}
+ {{- if eq $clean $target }}{{ $matches = true }}{{ end -}}
+ {{- end }}
+{{- end -}}
+{{- if $matches -}}true{{- end -}}
diff --git a/layouts/partials/sidebar_recursive.html b/layouts/partials/sidebar_recursive.html
new file mode 100644
index 00000000000..ed0de138968
--- /dev/null
+++ b/layouts/partials/sidebar_recursive.html
@@ -0,0 +1,83 @@
+{{- /* Recursively renders a sidebar tree node.
+ Leaf items become plain nav-links; branch items become collapsible
+ sections (Bootstrap 5 collapse). Params: item, current (page context),
+ level (nesting depth, 1-based), parentID (BS collapse parent). */ -}}
+{{- $item := .item -}}
+{{- $current := .current -}}
+{{- $level := .level | default 1 -}}
+{{- $parentID := .parentID | default "mysidebar" -}}
+{{- $title := $item.title | default "" -}}
+{{- $url := $item.url | default "" -}}
+{{- $folderitems := $item.folderitems | default (slice) -}}
+{{- $subfolders := $item.subfolders | default (slice) -}}
+{{- $subfolderitems := $item.subfolderitems | default (slice) -}}
+{{- $isBranch := false -}}
+{{- if eq $level 1 }}
+ {{- if gt (len $folderitems) 0 }}{{ $isBranch = true }}{{ end -}}
+{{- else if eq $level 3 }}
+ {{- $isBranch = true -}}
+{{- end -}}
+{{- $isActive := partial "sidebar_item_active.html" (dict "item" $item "current" $current) -}}
+{{- $isCurrent := false -}}
+{{- if $url }}
+ {{- $isCurrent = partial "sidebar_page_matches_url.html" (dict "page" $current "url" $url) -}}
+{{- end -}}
+
+{{- if not $isBranch -}}
+ {{- $href := partial "sidebar_resolve_url.html" (dict "url" $url) -}}
+
+
+ {{ $title }}
+
+
+{{- else -}}
+ {{- $currentID := printf "sb-%d-%s" $level ($title | anchorize) -}}
+ {{- $nextLevel := add $level 1 -}}
+
+
+ {{ $title }}
+
+
+
+{{- end -}}
diff --git a/layouts/partials/sidebar_resolve_url.html b/layouts/partials/sidebar_resolve_url.html
new file mode 100644
index 00000000000..7c3d5f7c954
--- /dev/null
+++ b/layouts/partials/sidebar_resolve_url.html
@@ -0,0 +1,11 @@
+{{- /* Resolves a sidebar URL for use in href attributes. External URLs
+ (http, https, mailto, #) are returned as-is; internal paths
+ are passed through relURL. */ -}}
+{{- $rawURL := .url | default "" -}}
+{{- $isExternal := or (hasPrefix $rawURL "http://") (or (hasPrefix $rawURL "https://") (or (hasPrefix $rawURL "mailto:") (hasPrefix $rawURL "#"))) -}}
+
+{{- if $isExternal -}}
+ {{- $rawURL -}}
+{{- else -}}
+ {{- $rawURL | relURL -}}
+{{- end -}}
diff --git a/layouts/partials/sidebar_url_clean.html b/layouts/partials/sidebar_url_clean.html
new file mode 100644
index 00000000000..a6cc3b3a4b9
--- /dev/null
+++ b/layouts/partials/sidebar_url_clean.html
@@ -0,0 +1,8 @@
+{{- /* Normalises a URL for comparison: trims whitespace and strips
+ leading/trailing slashes so "docs/foo/" matches "docs/foo". */ -}}
+{{- $url := "" -}}
+{{- with . }}{{ $url = printf "%v" . }}{{ end -}}
+{{- $url = strings.TrimSpace $url -}}
+{{- $url = strings.TrimPrefix "/" $url -}}
+{{- $url = strings.TrimSuffix "/" $url -}}
+{{- $url -}}