Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@
"version": "0.2",
"caseSensitive": true,
"words": [
"anchorize",
"Docsy",
"errorf",
"hugo",
"isset",
"relref",
"scrollspy",
"shortcode",
Expand Down
8 changes: 8 additions & 0 deletions assets/scss/_sidebar-tree.scss
Original file line number Diff line number Diff line change
Expand Up @@ -204,4 +204,12 @@

}
}

&-root-up-icon {
&::after {
font: var(--fa-font-solid);
content: fa-content($fa-var-caret-up);
padding-left: 0.5em;
}
}
}
130 changes: 110 additions & 20 deletions layouts/_partials/sidebar-tree.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
{{/* We cache this partial for bigger sites and set the active class client side. */ -}}
{{ $context := .context -}}
{{ $sidebarRoot := .sidebarRoot -}}
{{ $sidebarRootID := .sidebarRootID -}}
{{ $cacheSidebar := .cacheSidebar -}}

{{ $sidebarCacheLimit := .Site.Params.ui.sidebar_cache_limit | default 2000 -}}
{{ $shouldDelayActive := ge (len .Site.Pages) $sidebarCacheLimit -}}
{{ with $context -}}

{{/* When the sidebar is cached, "active" class is set client side. */ -}}
{{ $shouldDelayActive := $cacheSidebar -}}

<div id="td-sidebar-menu" class="td-sidebar__inner{{ if $shouldDelayActive }} d-none{{ end }}">
{{ if not .Site.Params.ui.sidebar_search_disable -}}
Expand Down Expand Up @@ -31,15 +36,21 @@
{{/* */ -}}

<nav class="td-sidebar-nav collapse
{{- if .Site.Params.ui.sidebar_search_disable }} td-sidebar-nav--search-disabled{{ end -}}
{{- if .Site.Params.ui.sidebar_menu_foldable }} foldable-nav{{ end -}}
" id="td-section-nav">
{{- if .Site.Params.ui.sidebar_search_disable }} td-sidebar-nav--search-disabled{{ end -}}
{{- if .Site.Params.ui.sidebar_menu_foldable }} foldable-nav{{ end }}" {{/**/ -}}
id="td-section-nav"
{{- if .Site.Params.ui.sidebar_root_enabled }} data-sidebar-root="{{ $sidebarRootID }}"{{ end -}}
>
{{ if and .Site.Params.ui.sidebar_lang_menu (gt (len .Site.Home.Translations) 0) -}}
<div class="td-sidebar-nav__section nav-item dropdown d-block d-lg-none">
{{ partial "navbar-lang-selector.html" . }}
</div>
{{ end -}}
{{ $navRoot := cond (and (ne .Params.toc_root true) (eq .Site.Home.Type "docs")) .Site.Home .FirstSection -}}
{{/* Use sidebar_root if provided, otherwise use the default navRoot */ -}}
{{ if $sidebarRoot -}}
{{ $navRoot = $sidebarRoot -}}
{{ end -}}
{{ $ulNr := 0 -}}
{{ $ulShow := .Site.Params.ui.ul_show | default 1 -}}
{{ $sidebarMenuTruncate := .Site.Params.ui.sidebar_menu_truncate | default 100 -}}
Expand All @@ -49,7 +60,10 @@
</nav>
</div>

{{- define "section-tree-nav-section" -}}
{{- end }}{{/* with $context */ -}}

{{ define "section-tree-nav-section" -}}
{{/* cSpell:ignore manuallink manuallinkrelref manuallinktitle */ -}}
{{ $s := .section -}}
{{ $p := .page -}}
{{ $shouldDelayActive := .shouldDelayActive -}}
Expand All @@ -59,34 +73,110 @@
{{ $ulShow := .ulShow -}}
{{ $active := and (not $shouldDelayActive) (eq $s $p) -}}
{{ $activePath := and (not $shouldDelayActive) (or (eq $p $s) ($p.IsDescendant $s)) -}}
{{ $show := cond (or (lt $ulNr $ulShow) $activePath (and (not $shouldDelayActive) (eq $s.Parent $p.Parent)) (and (not $shouldDelayActive) (eq $s.Parent $p)) (not $p.Site.Params.ui.sidebar_menu_compact) (and (not $shouldDelayActive) ($p.IsDescendant $s.Parent))) true false -}}
{{ $show := cond
(or
(lt $ulNr $ulShow)
$activePath
(and (not $shouldDelayActive) (eq $s.Parent $p.Parent))
(and (not $shouldDelayActive) (eq $s.Parent $p))
(not $p.Site.Params.ui.sidebar_menu_compact)
(and (not $shouldDelayActive) ($p.IsDescendant $s.Parent))
)
true false
-}}
{{ $mid := printf "m-%s" ($s.RelPermalink | anchorize) -}}
{{ $pages_tmp := where (union $s.Pages $s.Sections).ByWeight ".Params.toc_hide" "!=" true -}}
{{ $pages := $pages_tmp | first $sidebarMenuTruncate -}}
{{ $truncatedEntryCount := sub (len $pages_tmp) $sidebarMenuTruncate -}}

{{ if gt $truncatedEntryCount 0 -}}
{{ warnf "WARNING: %d sidebar entries have been truncated. To avoid this, increase `params.ui.sidebar_menu_truncate` to at least %d (from %d) in your config file. Section: %s"
$truncatedEntryCount (len $pages_tmp) $sidebarMenuTruncate $s.Path -}}
{{ end -}}

{{ $withChild := gt (len $pages) 0 -}}
{{ $manualLink := cond (isset $s.Params "manuallink") $s.Params.manualLink ( cond (isset $s.Params "manuallinkrelref") (relref $s $s.Params.manualLinkRelref) $s.RelPermalink) -}}
{{ $manualLinkTitle := cond (isset $s.Params "manuallinktitle") $s.Params.manualLinkTitle $s.Title -}}
<li class="td-sidebar-nav__section-title td-sidebar-nav__section{{ if $withChild }} with-child{{ else }} without-child{{ end }}{{ if $activePath }} active-path{{ end }}{{ if (not (or $show $p.Site.Params.ui.sidebar_menu_foldable )) }} collapse{{ end }}" id="{{ $mid }}-li">
{{ $manualLink :=
cond
(isset $s.Params "manuallink")
$s.Params.manualLink
(cond
(isset $s.Params "manuallinkrelref")
(relref $s $s.Params.manualLinkRelref)
$s.RelPermalink
)
-}}
{{ $manualLinkTitle :=
cond
(isset $s.Params "manuallinktitle")
$s.Params.manualLinkTitle
$s.Title
-}}
<li class="td-sidebar-nav__section-title td-sidebar-nav__section
{{- if $withChild }} with-child{{ else }} without-child{{ end -}}
{{ if $activePath }} active-path{{ end -}}
{{ if (not (or $show $p.Site.Params.ui.sidebar_menu_foldable )) }} collapse{{ end -}}
" {{/**/ -}}
id="{{ $mid }}-li" {{- /**/ -}}
>
{{ if (and $p.Site.Params.ui.sidebar_menu_foldable (ge $ulNr 1)) -}}
<input type="checkbox" id="{{ $mid }}-check"{{ if $activePath}} checked{{ end }}/>
<label for="{{ $mid }}-check"><a href="{{ $manualLink }}"{{ if ne $s.LinkTitle $manualLinkTitle }} title="{{ $manualLinkTitle }}"{{ end }}{{ with $s.Params.manualLinkTarget }} target="{{ . }}"{{ if eq . "_blank" }} rel="noopener"{{ end }}{{ end }} class="align-left ps-0 {{ if $active}} active{{ end }} td-sidebar-link{{ if $s.IsPage }} td-sidebar-link__page{{ else }} td-sidebar-link__section{{ end }}{{ if $treeRoot }} tree-root{{ end }}" id="{{ $mid }}">{{ with $s.Params.Icon}}<i class="{{ . }}"></i>{{ end }}<span class="{{ if $active }}td-sidebar-nav-active-item{{ end }}">{{ $s.LinkTitle }}</span></a></label>
<label for="{{ $mid }}-check">{{/**/ -}}
<a href="{{ $manualLink }}"
{{- if ne $s.LinkTitle $manualLinkTitle }} {{/**/ -}}
title="{{ $manualLinkTitle }}"
{{- end -}}
{{ with $s.Params.manualLinkTarget }} {{/**/ -}}
target="{{ . }}"
{{- if eq . "_blank" }} rel="noopener"{{ end -}}
{{ end }} {{/**/ -}}
class="align-left ps-0 {{ if $active}} active{{ end }} td-sidebar-link
{{- if $s.IsPage }} td-sidebar-link__page
{{- else }} td-sidebar-link__section
{{- end }}
{{- if $treeRoot }} tree-root{{ end }}" {{- /**/ -}}
id="{{ $mid }}" {{- /**/ -}}
>
{{- with $s.Params.Icon -}}
<i class="{{ . }}"></i>
{{- end -}}
<span class="{{ if $active }}td-sidebar-nav-active-item{{ end }}">
{{- $s.LinkTitle -}}
</span> {{- /**/ -}}
</a> {{- /**/ -}}
</label>
{{ else -}}
<a href="{{ $manualLink }}"{{ if ne $s.LinkTitle $manualLinkTitle }} title="{{ $manualLinkTitle }}"{{ end }}{{ with $s.Params.manualLinkTarget }} target="{{ . }}"{{ if eq . "_blank" }} rel="noopener"{{ end }}{{ end }} class="align-left ps-0{{ if $active}} active{{ end }} td-sidebar-link{{ if $s.IsPage }} td-sidebar-link__page{{ else }} td-sidebar-link__section{{ end }}{{ if $treeRoot }} tree-root{{ end }}" id="{{ $mid }}">{{ with $s.Params.Icon}}<i class="{{ . }}"></i>{{ end }}<span class="{{ if $active }}td-sidebar-nav-active-item{{ end }}">{{ $s.LinkTitle }}</span></a>
{{- end }}
{{- if $withChild }}
{{- $ulNr := add $ulNr 1 }}
<a href="{{ $manualLink }}"
{{- if ne $s.LinkTitle $manualLinkTitle }} title="{{ $manualLinkTitle }}"{{ end -}}
{{ with $s.Params.manualLinkTarget }} {{/**/ -}}
target="{{ . }}"
{{- if eq . "_blank" }} rel="noopener"{{ end -}}
{{ end }} {{/**/ -}}
class="align-left ps-0
{{- if $active}} active{{ end }} {{/**/ -}}
td-sidebar-link
{{- if $s.IsPage }} td-sidebar-link__page{{ else }} td-sidebar-link__section{{ end }}
{{- if $treeRoot }} tree-root{{ end }}" {{/**/ -}}
id="{{ $mid }}" {{- /**/ -}}
>
{{- with $s.Params.Icon -}}
<i class="{{ . }}"></i>
{{- end -}}
<span class="
{{- if $active }}td-sidebar-nav-active-item{{ end -}}
{{- if and $s.Params.sidebar_root .Site.Params.ui.sidebar_root_enabled }} td-sidebar-root-up-icon{{ end -}}
">
{{- $s.LinkTitle -}}
</span></a>
{{- end -}}
{{ if $withChild -}}
{{ $ulNr := add $ulNr 1 }}
<ul class="ul-{{ $ulNr }}{{ if (gt $ulNr 1)}} foldable{{end}}">
{{ range $pages -}}
{{ if (not (and (eq $s $p.Site.Home) (eq .Params.toc_root true))) -}}
{{ template "section-tree-nav-section" (dict "page" $p "section" . "shouldDelayActive" $shouldDelayActive "sidebarMenuTruncate" $sidebarMenuTruncate "ulNr" $ulNr "ulShow" $ulShow) }}
{{- end }}
{{ if (not (and (eq $s $p.Site.Home) (eq .Params.toc_root true))) -}}
{{ template "section-tree-nav-section" (dict "page" $p "section" . "shouldDelayActive" $shouldDelayActive "sidebarMenuTruncate" $sidebarMenuTruncate "ulNr" $ulNr "ulShow" $ulShow) }}
{{- end }}
{{- end }}
</ul>
{{- end }}
</li>
{{- end -}}
{{- end }}
47 changes: 35 additions & 12 deletions layouts/_partials/sidebar.html
Original file line number Diff line number Diff line change
@@ -1,23 +1,46 @@
{{/* The "active" toggle here may delay rendering, so we only cache this side bar menu for bigger sites. */ -}}

{{ $sidebarCacheLimit := .Site.Params.ui.sidebar_cache_limit | default 2000 -}}
{{ $shouldCache := ge (len .Site.Pages) $sidebarCacheLimit -}}
{{ $sidebarCacheTypeRoot := .Site.Params.ui.sidebar_cache_type_root | default false -}}
{{ if $shouldCache -}}
{{ $cacheSidebar := ge (len .Site.Pages) $sidebarCacheLimit -}}

{{/* Determine sidebar root and ID (if enabled) */ -}}
{{ $sidebarRoot := .FirstSection -}}
{{ $navRoot := cond (and (ne .Params.toc_root true) (eq .Site.Home.Type "docs")) .Site.Home .FirstSection -}}
{{ if .Site.Params.ui.sidebar_root_enabled -}}
{{ range .Ancestors.Reverse -}}
{{ if not (and .IsSection .Params.sidebar_root) -}}
{{ continue -}}
{{ end -}}
{{ $sidebarRoot = . -}}
{{/* Warn if sidebar_root is set on a top-level section */ -}}
{{ if or (eq . $.Site.Home) (eq . $navRoot) -}}
{{ warnf "sidebar_root is set on a top-level section (%s). This parameter is intended for nested sections only." .Path -}}
{{ end -}}
{{ end -}}
{{ end -}}
{{ $sidebarRootID := $sidebarRoot.RelPermalink -}}
{{ $args := dict
"context" .
"cacheSidebar" $cacheSidebar
"sidebarRoot" $sidebarRoot
"sidebarRootID" $sidebarRootID
-}}
{{ if $cacheSidebar -}}
{{ $mid := printf "m-%s" (.RelPermalink | anchorize) }}
<script>
$(function() {
$("#td-section-nav a").removeClass("active");
$("#td-section-nav #{{ $mid }}").addClass("active");
$("#td-section-nav #{{ $mid }}-li span").addClass("td-sidebar-nav-active-item");
$("#td-section-nav #{{ $mid }}").parents("li").addClass("active-path");
$("#td-section-nav li.active-path").addClass("show");
$("#td-section-nav #{{ $mid }}").addClass("active");
$("#td-section-nav #{{ $mid }}-li span").addClass("td-sidebar-nav-active-item");
$("#td-section-nav #{{ $mid }}").parents("li").addClass("active-path");
$("#td-section-nav li.active-path").addClass("show");
$("#td-section-nav li.active-path").children("input").prop('checked', true);
$("#td-section-nav #{{ $mid }}-li").siblings("li").addClass("show");
$("#td-section-nav #{{ $mid }}-li").children("ul").children("li").addClass("show");
$("#td-sidebar-menu").toggleClass("d-none");
$("#td-section-nav #{{ $mid }}-li").siblings("li").addClass("show");
$("#td-section-nav #{{ $mid }}-li").children("ul").children("li").addClass("show");
$("#td-sidebar-menu").toggleClass("d-none");
});
</script>
{{ partialCached "sidebar-tree.html" . .FirstSection.RelPermalink }}
{{ partialCached "sidebar-tree.html" $args $sidebarRootID }}
{{ else -}}
{{ partial "sidebar-tree.html" . }}
{{ partial "sidebar-tree.html" $args }}
{{- end }}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"scripts": {
"_build": "npm run cd:docs -- _build",
"_cd:docs": "cd userguide &&",
"_check:format": "npx prettier --check *.md",
"_check:format": "npx prettier --check *.md tasks",
"_check:links": "npm run cd:docs -- _check:links",
"_commit:public": "npm run cd:docs -- _commit:public",
"_cp:bs-rfs": "npx cpy 'node_modules/bootstrap/scss/vendor/*' assets/_vendor/bootstrap/scss/",
Expand Down
124 changes: 124 additions & 0 deletions tasks/sidebar-root-feature.plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
---
title: Sidebar-root feature development notes
date: 2025-10-13
---

# Sidebar-root feature development notes

<!-- cSpell:ignore sidenav -->

This is a feature description for issue [Sidenav support for section-as-root
#2328][#2328], and some development notes. Being development notes, they might
not fully capture the final implementation.

Add a `sidebar_root` front matter parameter that can be set in a section's
`_index.md` to make that section the root of sidebar navigation, useful for
deeply nested documentation sections.

## Feature characteristics

- New `sidebar_root: true` parameter set in section `_index.md`. This marks the
section as a sidebar root.
- When viewing pages **under** a sidebar root section (not the section index
page), show only that section and descendants in the sidebar. That is, the
sidebar becomes rooted in this designated sidebar root.
- When viewing a sidebar-root section index page, the parent's sidebar tree is
shown.
- Include navigation out of a sidebar-root section.
- Work alongside existing `toc_root` feature (not replace it)
- Warn if `sidebar_root` is set on a top-level section (including site home in
docs-only sites)

## Feature interaction: `toc_root`

Since `sidebar_root` sections are always contained within a top-level section
(whether `toc_root` or default), pages only need to determine their sidebar root
within their already-established top-level boundary. No special interaction
handling is required.

> NOTE: it might be possible to merge the two features, but that's a future
> improvement.

## Implementation Changes

### 1. Update `layouts/_partials/sidebar.html` - Find sidebar root and update cache key

- Walk up page ancestors to find section with `sidebar_root: true`
- Use sidebar_root section's permalink as cache key
- Warn if `sidebar_root` is set on a top-level section
- Pass `sidebarRoot` to `sidebar-tree.html` as parameter

### 2. Add link back to sidebar-root section index page

The current implementation of the sidebar tree, already has the sidebar tree
"heading" as a link. This can naturally be used to link back to the sidebar-root
section index page.

**In `layouts/_partials/sidebar-tree.html`:**

- When `sidebar_root` is active, add a link at the top of the sidebar navigation
- Link should point to the parent section of the sidebar_root
- Use the `td-sidebar-root-up-icon` CSS class for the up-arrow icon
- Style the link to be visually distinct from regular navigation items

### 3. Modify `layouts/_partials/sidebar-tree.html` - Add breadcrumb navigation (OPTIONAL)

- When a `sidebar_root` is active (not the top-level section), add a breadcrumb
section
- Show "← Back to [Parent Section]" link(s) above the main navigation tree
- Use appropriate styling to distinguish from regular navigation items

### 4. Modify `layouts/_partials/sidebar-tree.html` - Use sidebar_root for $navRoot (COMPLETED)

- Receive `sidebarRoot` as parameter from sidebar.html
- Use `sidebarRoot` as `$navRoot` when provided
- No duplicate ancestor walk needed (already done in sidebar.html)

### 5. Add CSS styling (COMPLETED)

**In `assets/scss/_sidebar-tree.scss`:**

- Created `td-sidebar-root-up-icon` class with `::after` pseudo-element
- Uses Font Awesome caret-up icon
- Consistent spacing and alignment with other sidebar icons

## Testing Considerations

- [ ]Test with deeply nested sections (3+ levels)
- [ ] Verify caching works correctly with sidebar_root active
- [ ] Ensure foldable menu behavior still works
- [ ] Test with both `sidebar_menu_compact` enabled and disabled
- [ ] Verify it doesn't conflict with existing `toc_root` functionality

## Docsy User Guide usage example

Assume the following front matter in the User Guide:
`/content/en/docs/adding-content/_index.md`:

```yaml
---
title: Content and Customization
# ...
sidebar_root: true
---
```

When viewing any page under `/docs/adding-content/` (such as
`/docs/adding-content/content/`), the sidebar will show only the "Content and
Customization" section and its children, instead of the full docs navigation
tree. This makes the sidebar more focused for users working within this
subsection.

Note that viewing the index page of `/docs/adding-content/` will still show the
full docs navigation tree.

### To-dos

- [x] Step 1: Implement sidebar_root lookup and cache key logic in sidebar.html
- [ ] Step 2: Add link back to site root section index page
- [ ] Step 3: Add breadcrumb navigation UI (OPTIONAL/FUTURE)
- [x] Step 4: Use sidebar_root for $navRoot calculation in sidebar-tree.html
- [x] Step 5: Add CSS styling for up-arrow icon
- [ ] Testing: Verify all navigation scenarios work correctly

[#2328]: https://github.com/google/docsy/issues/2328
Loading