Skip to content

feat(sidebar): carry a page's icon into the menu entry - #2102

Merged
markdumay merged 2 commits into
mainfrom
feat/sidebar-page-icons
Aug 1, 2026
Merged

feat(sidebar): carry a page's icon into the menu entry#2102
markdumay merged 2 commits into
mainfrom
feat/sidebar-page-icons

Conversation

@markdumay

Copy link
Copy Markdown
Collaborator

The sidebar renderer already supports a leading icon, but only for data-file menus. A site whose sidebar is derived from the page tree can never show one.

The gap

assets/sidebar.html reads a pre field for a leading icon and honors it at every level — 86, 124, 188, 200, 372, 390, 416, 473, 503.

But assets/helpers/sidebar-menu-entry.html:22, which builds entries for the page-hierarchy sidebar (assets/live-pages.html:244), only ever sets two keys:

{{- $entry := dict "title" $page.LinkTitle "link" $page.RelPermalink -}}

So pre is never populated on that path, and no amount of frontmatter can surface an icon. A site using a data/*.yml menu gets icons; a site using the page tree cannot.

The change

Populate pre from the page's icon param, using the merge already used a few lines below for pages:

{{- with $page.Params.icon -}}
    {{- $entry = merge $entry (dict "pre" .) -}}
{{- end -}}

The key is omitted when unset, so the renderer's with guards stay false rather than receiving an empty string. Output for pages without an icon is unchanged.

Evidence

Built a consuming site with ~180 pages carrying icon: frontmatter, before and after, via a module replacement:

Section Sidebar items Icons before Icons after
/architecture/ 126 0 117
/engineering/ 63 0 56
/reference/ 232 0 58

Every rendered icon is a self-contained <svg><path><use href> count is 0 in all three.

That last number is the one that matters here. assets/sidebar.html documents at length that icons in this partial must render in svg mode, because the partial is partialCached and symbols mode registers its <symbol> as a per-page side effect — the cached output would then carry <use href="#id"> with nothing behind it. The comment also warns the failure is self-masking, since an icon shared with the page body still resolves. Populating pre inherits the existing render sites' "mode" "svg", and the zero above confirms it rather than assuming it.

pnpm test passes.

Context

Found while adding sidebar icons to two sites built on Hinode. One drives its docs sidebar from data/docs.yml and needed only four lines of data; the other uses the page hierarchy and could not do it at all. This closes that asymmetry.

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.
@netlify

netlify Bot commented Aug 1, 2026

Copy link
Copy Markdown

Deploy Preview for gethinode-demo ready!

Name Link
🔨 Latest commit 82d42a7
🔍 Latest deploy log https://app.netlify.com/projects/gethinode-demo/deploys/6a6df218e3dabf0007a22264
😎 Deploy Preview https://deploy-preview-2102--gethinode-demo.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

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.<Type>`
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.
@markdumay

Copy link
Copy Markdown
Collaborator Author

Pushed a follow-up that makes this opt-in — thanks for catching the demo.

The deploy preview showed the problem clearly: mod-docs sets icon on exactly one page (Card), so the sidebar rendered one lone icon among ~30 text rows. icon is authored for a page's card, and the first commit made it silently drive the sidebar too — which would do the same to every existing Hinode site.

Now off by default, with a configurable depth

[navigation]
    sidebarIcons = false      # site-wide switch
    sidebarIconLevel = 1      # deepest level that may show an icon

A depth rather than a bool because icons on the top level only is the common case — that was the original request that started this.

Both settings cascade through per-page frontmatter and per-type pages.<Type> defaults, resolved by a new utilities/GetSidebarIcons.html that mirrors the existing utilities/GetIncludeTOC.html, so this adds no new configuration idiom.

One thing worth flagging

Levels are counted as the sidebar renders them, not as the partial recurses. assets/sidebar.html:75-77 drops the walk's root entry and promotes its children, so the root is never a visible row. Counting from the recursion root made sidebarIconLevel = 1 select the entry that gets thrown away — it produced zero icons. Only measuring caught it; the code read correctly.

Evidence

Icon counts read out of the rendered sidebar on a site with ~180 pages carrying icon:

Setting /architecture/ /engineering/
default (unset) 0 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 — so it is the top row, not an arbitrary subset.

Every rendered icon remains a self-contained <svg><path> with a <use href> count of 0, so the partialCached/symbols constraint documented at the top of assets/sidebar.html still holds.

pnpm test passes. The demo should render exactly as it does on main now, since the defaults leave it off.

Follow-on

The two settings need documenting on gethinode.com — I will raise that against gethinode/gethinode.com once a version number exists to reference, since the table there tags each setting with the release that introduced it.

@markdumay
markdumay merged commit 636e153 into main Aug 1, 2026
23 of 24 checks passed
@markdumay
markdumay deleted the feat/sidebar-page-icons branch August 1, 2026 13:35
@markdumay

Copy link
Copy Markdown
Collaborator Author

🎉 This PR is included in version 3.13.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

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.

1 participant