From 6ea6d0ecc75bb2dced587fa2ab814553b2313e7a Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Sat, 1 Aug 2026 14:18:46 +0200 Subject: [PATCH 1/2] feat(sidebar): carry a page's icon into the menu entry The sidebar renderer already reads a `pre` field for a leading icon and honors it at every level, but the page-hierarchy entry builder only ever set `title` and `link`. A site whose sidebar is derived from the page tree could therefore never show one, however its pages were authored, while a site driving the same sidebar from a data file could. Populates `pre` from the page's `icon` param, using the merge already used below for `pages`. The key is omitted when unset so the renderer's `with` guards stay false rather than seeing an empty string, which keeps existing output byte-identical for pages without an icon. Icons inherit the svg mode the sidebar's render sites already pass, so this does not touch the partialCached/symbols constraint documented at the top of assets/sidebar.html. --- .../assets/helpers/sidebar-menu-entry.html | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/layouts/_partials/assets/helpers/sidebar-menu-entry.html b/layouts/_partials/assets/helpers/sidebar-menu-entry.html index ff97e5ff..26214f0b 100644 --- a/layouts/_partials/assets/helpers/sidebar-menu-entry.html +++ b/layouts/_partials/assets/helpers/sidebar-menu-entry.html @@ -8,7 +8,9 @@ - reverse: Whether to reverse sort order (default: false) - seen: Slice of ancestor permalinks in the current walk, used as cycle guard (internal) - Returns: Menu entry dict with title and pages (optional array of nested entries) + Returns: Menu entry dict with title, link, an optional pre (leading icon, + taken from the page's `icon` param), and optional pages (array of nested + entries) */}} {{- $page := .page -}} @@ -21,6 +23,15 @@ {{- /* Include the full RelPermalink; sidebar will recognize leading "/" as absolute */ -}} {{- $entry := dict "title" $page.LinkTitle "link" $page.RelPermalink -}} +{{- /* Carry the page's icon as "pre", the field the sidebar renderer already + reads for a leading icon on menu-defined entries. Without this a page-derived + sidebar can never show one, however the page is authored. Omitted when unset + so the renderer's `with` guards stay false rather than seeing an empty + string. */ -}} +{{- with $page.Params.icon -}} + {{- $entry = merge $entry (dict "pre" .) -}} +{{- end -}} + {{- /* Get children from grouped hierarchy */ -}} {{- $children := index $grouped $page.RelPermalink | default slice -}} {{- /* Guard against cycles: the grouped map keys pages by permalink, so any permalink collision From 82d42a7a909edd957dbfe20d7807db1fdd051f67 Mon Sep 17 00:00:00 2001 From: Mark Dumay <61946753+markdumay@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:17:47 +0200 Subject: [PATCH 2/2] feat(sidebar): gate page-derived icons behind a config setting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the previous commit, which set `pre` unconditionally. That silently changed every existing site: `icon` is authored for a page's card, so sidebars gained icons nobody asked for, and only on the pages that happen to set one. Hinode's own demo showed the failure — mod-docs sets `icon` on a single page, so the sidebar rendered one lone icon among thirty text rows. Default is now off, and the depth is configurable because icons on the top level alone is the common case: navigation.sidebarIcons bool, site-wide (default false) navigation.sidebarIconLevel int, deepest level (default 1) Both cascade through per-page frontmatter and per-type `pages.` defaults, resolved by a new `utilities/GetSidebarIcons.html` that mirrors `utilities/GetIncludeTOC.html`. Levels are counted as the sidebar renders them. `assets/sidebar.html` drops the walk's root entry and promotes its children, so the root is never a visible row — counting from the recursion root would have made `sidebarIconLevel = 1` select an entry that is thrown away, which is exactly what it did before this was measured. Verified on a site with ~180 pages carrying `icon`, reading icon counts out of the rendered sidebar: default architecture 0 engineering 0 level 1 13 7 level 2 59 43 level 9 117 56 At level 1 the thirteen glyphs are distinct and match the thirteen top-level sections exactly. --- config/_default/params.toml | 2 + .../assets/helpers/sidebar-menu-entry.html | 29 ++++++++-- layouts/_partials/assets/live-pages.html | 5 ++ .../_partials/utilities/GetSidebarIcons.html | 55 +++++++++++++++++++ 4 files changed, 85 insertions(+), 6 deletions(-) create mode 100644 layouts/_partials/utilities/GetSidebarIcons.html diff --git a/config/_default/params.toml b/config/_default/params.toml index 749e0bda..2f7e5404 100644 --- a/config/_default/params.toml +++ b/config/_default/params.toml @@ -151,6 +151,8 @@ breadcrumb = true toc = true sidebar = true + sidebarIcons = false + sidebarIconLevel = 1 size = "md" startLevel = 2 endLevel = 3 diff --git a/layouts/_partials/assets/helpers/sidebar-menu-entry.html b/layouts/_partials/assets/helpers/sidebar-menu-entry.html index 26214f0b..703db971 100644 --- a/layouts/_partials/assets/helpers/sidebar-menu-entry.html +++ b/layouts/_partials/assets/helpers/sidebar-menu-entry.html @@ -7,6 +7,8 @@ - sortField: Field to sort children by (e.g., "title", "date", "weight") - reverse: Whether to reverse sort order (default: false) - seen: Slice of ancestor permalinks in the current walk, used as cycle guard (internal) + - iconLevel: Deepest 1-based level that may show an icon; 0 disables them + (resolved once by the caller via utilities/GetSidebarIcons.html) Returns: Menu entry dict with title, link, an optional pre (leading icon, taken from the page's `icon` param), and optional pages (array of nested @@ -18,6 +20,7 @@ {{- $sortField := .sortField | default "title" -}} {{- $reverse := .reverse | default false -}} {{- $seen := .seen | default slice -}} +{{- $iconLevel := .iconLevel | default 0 -}} {{- /* Create menu entry with title and absolute link */ -}} {{- /* Include the full RelPermalink; sidebar will recognize leading "/" as absolute */ -}} @@ -25,11 +28,25 @@ {{- /* Carry the page's icon as "pre", the field the sidebar renderer already reads for a leading icon on menu-defined entries. Without this a page-derived - sidebar can never show one, however the page is authored. Omitted when unset - so the renderer's `with` guards stay false rather than seeing an empty - string. */ -}} -{{- with $page.Params.icon -}} - {{- $entry = merge $entry (dict "pre" .) -}} + sidebar can never show one, however the page is authored. + + Gated on depth, and off unless the site opts in: `icon` is authored for a + page's card, so rendering it here unconditionally would put icons into the + sidebars of existing sites that never asked for them - and only on the pages + that happen to set one, which reads as a bug rather than a feature. Icons on + the top level alone is the common case, hence a level rather than a bool. + + Levels are counted as the sidebar renders them, not as this partial recurses. + assets/sidebar.html drops the walk's root entry and promotes its children to + the top of the list, so the root is never a visible row; `len $seen` is + therefore already the rendered 1-based level, and level 1 is the first row a + reader sees. Omitted when unset or out of range so the renderer's `with` + guards stay false rather than seeing an empty string. */ -}} +{{- $level := len $seen -}} +{{- if le $level $iconLevel -}} + {{- with $page.Params.icon -}} + {{- $entry = merge $entry (dict "pre" .) -}} + {{- end -}} {{- end -}} {{- /* Get children from grouped hierarchy */ -}} @@ -68,7 +85,7 @@ {{- /* Recursively build nested menu entries */ -}} {{- $pages := slice -}} {{- range $children -}} - {{- $childEntry := partial "assets/helpers/sidebar-menu-entry" (dict "page" . "grouped" $grouped "sortField" $sortField "reverse" $reverse "seen" $seen) -}} + {{- $childEntry := partial "assets/helpers/sidebar-menu-entry" (dict "page" . "grouped" $grouped "sortField" $sortField "reverse" $reverse "seen" $seen "iconLevel" $iconLevel) -}} {{- $pages = $pages | append $childEntry -}} {{- end -}} {{- $entry = merge $entry (dict "pages" $pages) -}} diff --git a/layouts/_partials/assets/live-pages.html b/layouts/_partials/assets/live-pages.html index a65b1972..434defba 100644 --- a/layouts/_partials/assets/live-pages.html +++ b/layouts/_partials/assets/live-pages.html @@ -240,12 +240,17 @@ {{- $pages = $result -}} {{/* Build menu structure from sorted pages and hierarchy */}} + {{- /* Resolve the icon depth once against the page being rendered, + not against each entry: the cascade is keyed on the current + page's type, while the entries are the pages being listed. */ -}} + {{- $iconLevel := partial "utilities/GetSidebarIcons.html" (or $args.page page) -}} {{- range $rootPages -}} {{- $menuEntry := partial "assets/helpers/sidebar-menu-entry" (dict "page" . "grouped" $grouped "sortField" $sortField "reverse" $reverse + "iconLevel" $iconLevel ) -}} {{- $menu = $menu | append $menuEntry -}} {{- end -}} diff --git a/layouts/_partials/utilities/GetSidebarIcons.html b/layouts/_partials/utilities/GetSidebarIcons.html new file mode 100644 index 00000000..3c816f74 --- /dev/null +++ b/layouts/_partials/utilities/GetSidebarIcons.html @@ -0,0 +1,55 @@ +{{- /* + Resolve how deep the page-hierarchy sidebar should render entry icons. + + Returns the deepest 1-based level that may show an icon, or 0 for none. + Callers compare an entry's level against it, so "off" needs no separate + boolean at the call site. + + Resolution mirrors utilities/GetIncludeTOC.html: a site-wide switch, then a + per-page override, then a per-type default. Both the switch and the level + cascade, so a section can enable icons without restating the depth, or + change the depth without restating the switch. + + navigation.sidebarIcons bool, site-wide (default false) + navigation.sidebarIconLevel int, deepest level (default 1) + .Params.sidebarIcons bool, per page + .Params.sidebarIconLevel int, per page + pages..sidebaricons bool, per content type + pages..sidebariconlevel int, per content type + + Defaults to off. A page's `icon` param is authored for its card, so + enabling this by default would put icons in the sidebars of existing sites + that never asked for them - and only on the pages that happen to set one, + which looks like a bug rather than a feature. + + Hugo lowercases the keys of the `pages.` maps, hence `sidebaricons` + and `sidebariconlevel` there but camelCase everywhere else. +*/ -}} + +{{- $enabled := site.Params.navigation.sidebarIcons | default false -}} +{{- $level := site.Params.navigation.sidebarIconLevel | default 1 -}} + +{{- $typeDefaults := index site.Params.pages .Type -}} +{{- with $typeDefaults -}} + {{- if isset . "sidebaricons" -}} + {{- $enabled = index . "sidebaricons" -}} + {{- if ne (printf "%T" $enabled) "bool" -}} + {{- errorf "Expected bool value in site parameters: pages.%s.sidebarIcons" $.Type -}} + {{- end -}} + {{- end -}} + {{- if isset . "sidebariconlevel" -}} + {{- $level = index . "sidebariconlevel" -}} + {{- end -}} +{{- end -}} + +{{- if isset .Params "sidebaricons" -}} + {{- $enabled = .Params.sidebaricons -}} + {{- if ne (printf "%T" $enabled) "bool" -}} + {{- errorf "Expected bool value in page parameters: sidebarIcons (%s)" .RelPermalink -}} + {{- end -}} +{{- end -}} +{{- if isset .Params "sidebariconlevel" -}} + {{- $level = .Params.sidebariconlevel -}} +{{- end -}} + +{{- return cond $enabled (int $level) 0 -}}