Nested, collapsible sidebar navigation (NavPage groups + native <details>) - #289
Conversation
…ils>) A nav section's pages can now themselves be groups, so a long section (e.g. docs' 33-entry "UI Patterns") renders as collapsible category sub-groups instead of one flat list. Sections and groups are native <details>/<summary>, so collapse needs zero JS and the disclosure chevron comes free from PicoCSS. - config: NavPage gains Pages (recursive) + Collapsed; presence of Pages makes an entry a group. Backward compatible — flat configs leave Pages empty. - site: discoverFromConfig recurses via buildNavPageNode, registering every nested leaf in m.pages (the registration invariant — a missed leaf 404s in site mode). PageNode gains Collapsed + ContainsPath; search-index section lookup now descends the whole subtree. - server: renderSidebar emits sections + groups as <details> honoring `collapsed` and auto-opening the branch that holds the active page. Pico's details margins/colors are neutralized with id-scoped overrides; top-level summaries keep class nav-section-title, groups use nav-group-title. Tests: nested leaves serve 200, group/section structure + classes, and collapse/auto-open state. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code ReviewOverall this is a clean, well-scoped implementation. The zero-JS Bug: empty-path leaf node renders as a dead link
<li><a href="" class="nav-page-link">Placeholder</a></li>A click on that link navigates to the site root, which is a silent bug. Suggest validating at // in writeNavChildren, leaf branch:
if node.Path == "" {
continue // pure group with no children — skip, invalid config
}Or return an error from HTML encoding of titles and paths
Worth fixing globally with b.WriteString(fmt.Sprintf(`<summary class="nav-group-title">%s</summary>`, html.EscapeString(node.Title)))
// and for hrefs:
b.WriteString(fmt.Sprintf(`<li><a href="%s" class="nav-page-link%s">%s</a></li>`,
node.Path, activeClass, html.EscapeString(node.Title)))(Import Minor:
|
From the claude-review on #289: - HTML-escape section/group/leaf titles via a package-level escapeHTML helper (the local `html` strings.Builder in renderSidebar shadows the stdlib html package, so the helper lives at package scope). Fixes invalid markup for titles containing & < > " and closes an injection vector for config authors. - Skip a leaf node with an empty Path in writeNavChildren — a node with no path and no children would otherwise emit <a href=""> (a dead link to root). - Trim the long inline CSS comment to the key Pico-override caveat. - Tests: depth-3 nesting registration + search-index section resolution for a nested leaf, and a group carrying both a landing-page Path and children. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review — Nested, collapsible sidebar navigationNice, well-scoped feature. The zero-JS approach with native Issues1. In b.WriteString(fmt.Sprintf(`<li><a href="%s" class="nav-page-link%s">%s</a></li>`,
node.Path, activeClass, escapeHTML(node.Title)))
b.WriteString(fmt.Sprintf(`<li><a href="%s" class="nav-page-link%s">%s</a></li>`,
escapeHTML(node.Path), activeClass, escapeHTML(node.Title)))2. Missing test: empty
Observations / suggestions (non-blocking)3. Whitespace-only alignment changes in The first ~30 lines of the 4. No recursion depth limit in In practice a nav tree will never be deep enough to blow the stack, but a deeply pathological or accidentally self-referential YAML config would cause a crash with no useful error message. A depth guard (e.g. max 10 levels) with a clear error would make this more robust. Low priority. 5. #tinkerdown-sidebar .nav-section-title,
#tinkerdown-sidebar .nav-group-title {
cursor: pointer;
list-style: none; /* hides the default bullet but not the disclosure triangle */
}
What's done well
Summary: The main actionable item is escaping |
…ff noise Second round on #289: - Escape node.Path in the href attribute too (was only escaping titles) — a path containing " would otherwise break the attribute. - buildNavPageNode now errors when a NavPage has neither path nor pages, instead of silently dropping it from the sidebar; the render-time skip stays as defense-in-depth. Test added. - Revert the incidental gofmt whitespace re-alignment of the Config / SourceConfig structs so config.go's diff is just the NavPage change. - Note in CSS that Pico draws the chevron (summary::after) and already hides the native marker; list-style only clears the bullet. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code ReviewOverall this is a well-structured feature. The recursive design is clean, HTML escaping is handled correctly throughout, the backward-compatibility story is solid, and the test suite covers the key invariants. A few things to flag: Bug: group landing page is sidebar-invisibleA
Minimal fix — in b.WriteString(fmt.Sprintf(`<summary class="nav-group-title">%s</summary>`, escapeHTML(node.Title)))
if node.Path != "" {
activeClass := ""
if node.Path == currentPath {
activeClass = " active"
}
b.WriteString(fmt.Sprintf(`<li><a href="%s" class="nav-page-link%s">%s</a></li>`, escapeHTML(node.Path), activeClass, escapeHTML(node.Title)))
}
writeNavChildren(b, node.Children, currentPath)
|
- A group that carries its own path (landing page) now renders a clickable, active-aware link to it as the first item inside the group. Refactored the renderer into writeNavNode/writeNavLink so the link sits inside the group's <ul> (valid markup) and recursion stays clean. Test added. - Remove the redundant `cursor: pointer; list-style: none` rule — Pico already sets both on <summary> and hides the native marker; replaced with a comment. Remaining review suggestions (recursion depth guard, empty-<ul> micro-opt) are non-issues for a hand-authored config tree and intentionally not implemented. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Code Review: Nested, Collapsible Sidebar NavigationOverall this is a solid, well-scoped feature. The zero-JS Bugs / Correctness
html.WriteString(fmt.Sprintf(`<li><a href="%s">%s</a></li>`, crumb.Path, crumb.Title))The PR correctly adds
b.WriteString(fmt.Sprintf(`<li><a href="%s" ...>%s</a></li>`, escapeHTML(node.Path), ...))
Code Smell
// Defined at package scope (not inside renderSidebar, where the local `html`
// strings.Builder shadows the stdlib html package).
func escapeHTML(s string) string { return html.EscapeString(s) }The root cause is that CSS Nits
#tinkerdown-sidebar .nav-group-item {
display: list-item; /* <li> is already display:list-item */
margin: 0;
padding: 0;
}The Hardcoded indent values could drift Test Coverage — What's MissingThe existing tests are good. Two gaps worth noting:
Minor Positives Worth Calling Out
🤖 Generated with Claude Code |
What
A nav section's pages can now themselves be groups, so a long section renders as collapsible category sub-groups instead of one flat list. Sections and groups are native
<details>/<summary>, so collapse needs zero JS and the disclosure chevron comes free from PicoCSS.Motivated by the docs site's "UI Patterns" section, which lists 33 pattern pages as a single flat sidebar list. With this, they nest under their 7 categories.
Changes
internal/config/config.go):NavPagegainsPages(recursive) +Collapsed. Presence ofPagesmakes an entry a group. Backward compatible — existing flat configs leavePagesempty.internal/site/manager.go):discoverFromConfigrecurses viabuildNavPageNode, registering every nested leaf inm.pages(the registration invariant — a missed leaf 303s to home in site mode).PageNodegainsCollapsed+ContainsPath; the search-index section lookup now descends the whole subtree.internal/server/server.go):renderSidebaremits sections + groups as<details>honoringcollapsedand auto-opening the branch that holds the active page. Pico's<details>margins/colors are neutralized with id-scoped overrides; top-level summaries keepclass="nav-section-title", groups usenav-group-title.Tests
internal/server/nested_nav_test.go: nested leaves serve 200, group/section structure + classes, and collapse/auto-open state. Full CI suite (go test -tags=ci -skip='E2E|e2e' ./...) green.Verification
Built the docs site against this branch and verified in a real browser (chromedp): UI Patterns → 7 collapsible category groups, active category auto-opens, all 33 leaves serve 200, top-level IA section count unchanged.
🤖 Generated with Claude Code