fix(docs): folder-index pages expand the sidebar and highlight — same rule for every page
The filter manual is the docs tree's first FOLDER-index page: it serves at /docs/user/filters while its nav entry carries /index (/docs/user/filters/index). The sidebar's two checks — group expansion (containsURL) and active-link styling (docsNavClass) — compared URLs EXACTLY, so the folder URL matched neither: no submenu expansion, no highlight (2026-11-01 user report: "the left sidebar should open to the submenu and highlight the page"). Fix is structural, not per-page: both checks normalize folder-index URLs (a trailing /index is the same page as its directory), so EVERY folder-index page — current and future — expands and highlights like a file page. No per-page registration exists anywhere in the docs system to redo: the sidebar derives from the docs tree, and this was the one URL shape its equality rule missed. Pins: navigation (section + group expand for the folder URL, group present) and docsNavClass (folder URL highlights the Overview item, normalization symmetric, file pages keep their highlight, no cross-highlighting).
This commit is contained in:
1 parent
a4dc03984c
commit
0d613ba0e3
4 files changed
+82
-4
No files matched your search
@@ -321,10 +321,23 @@ func (h *DocsHandler) BuildNavigation(currentPath string) *templates.Navigation
|
|||||||
return nav
|
return nav
|
||||||
}
|
}
|
||||||
|
|
||||||
// containsURL reports whether currentPath is one of the given doc URLs.
|
// normalizeDocURL — folder-index pages SERVE at their directory URL
|
||||||
|
// (/docs/user/filters) while their nav entry carries the /index suffix
|
||||||
|
// (/docs/user/filters/index): same page, two spellings. Both sidebar
|
||||||
|
// checks (expansion + highlight) compare normalized forms, so folder
|
||||||
|
// pages expand and highlight like file pages — no per-page special
|
||||||
|
// cases (2026-11-01, the filter manual's folder).
|
||||||
|
func normalizeDocURL(u string) string {
|
||||||
|
u = strings.TrimRight(u, "/")
|
||||||
|
return strings.TrimSuffix(u, "/index")
|
||||||
|
}
|
||||||
|
|
||||||
|
// containsURL reports whether currentPath is one of the given doc URLs
|
||||||
|
// (folder-index URLs normalized — see normalizeDocURL).
|
||||||
func containsURL(currentPath string, urls []string) bool {
|
func containsURL(currentPath string, urls []string) bool {
|
||||||
|
current := normalizeDocURL(currentPath)
|
||||||
for _, u := range urls {
|
for _, u := range urls {
|
||||||
if currentPath == u {
|
if current == normalizeDocURL(u) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"bookhoard/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPlaceDoc(t *testing.T) {
|
func TestPlaceDoc(t *testing.T) {
|
||||||
@@ -205,3 +207,41 @@ func TestBuildNavigationNothingCurrent(t *testing.T) {
|
|||||||
t.Errorf("expected zero sections open when nothing matches (docs home / 404 renders fully closed), got %d", open)
|
t.Errorf("expected zero sections open when nothing matches (docs home / 404 renders fully closed), got %d", open)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuildNavigationFolderIndex (2026-11-01): folder-index pages serve
|
||||||
|
// at their directory URL (/docs/user/filters) while their nav entries
|
||||||
|
// carry /index (/docs/user/filters/index) — the sidebar must EXPAND the
|
||||||
|
// containing section/group and HIGHLIGHT the index item for the folder
|
||||||
|
// URL, exactly like a file page. The filter manual's folder is the
|
||||||
|
// first page of this shape.
|
||||||
|
func TestBuildNavigationFolderIndex(t *testing.T) {
|
||||||
|
// The REAL docs tree: the Filters group only exists because the
|
||||||
|
// filter manual (guide/cookbook/fields) lives in user/filters/.
|
||||||
|
handler := NewDocsHandler("../../docs")
|
||||||
|
nav := handler.BuildNavigation("/docs/user/filters")
|
||||||
|
|
||||||
|
var userGuide *templates.NavSection
|
||||||
|
for i := range nav.Sections {
|
||||||
|
if nav.Sections[i].Title == "User Guide" {
|
||||||
|
userGuide = &nav.Sections[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if userGuide == nil {
|
||||||
|
t.Fatal("User Guide section missing")
|
||||||
|
}
|
||||||
|
if userGuide.Collapsed {
|
||||||
|
t.Error("User Guide section must expand for /docs/user/filters")
|
||||||
|
}
|
||||||
|
var filters *templates.NavGroup
|
||||||
|
for i := range userGuide.Groups {
|
||||||
|
if userGuide.Groups[i].Title == "Filters" {
|
||||||
|
filters = &userGuide.Groups[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filters == nil {
|
||||||
|
t.Fatal("Filters group missing from User Guide")
|
||||||
|
}
|
||||||
|
if filters.Collapsed {
|
||||||
|
t.Error("Filters group must expand for its folder URL")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -144,3 +144,23 @@ func TestDocsNotFoundRenders(t *testing.T) {
|
|||||||
t.Error("404 page must not pull the tailwind CDN")
|
t.Error("404 page must not pull the tailwind CDN")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// docsNavClass folder-index normalization (2026-11-01): folder-index
|
||||||
|
// pages serve at their directory URL while nav entries carry /index —
|
||||||
|
// the Overview item must highlight for the folder URL, file pages keep
|
||||||
|
// their own highlight, and cross-highlighting stays impossible.
|
||||||
|
func TestDocsNavClassFolderIndex(t *testing.T) {
|
||||||
|
active := docsNavClass("/docs/user/filters", "/docs/user/filters/index", "pl-9")
|
||||||
|
if !strings.Contains(active, "bg-brand/15") {
|
||||||
|
t.Errorf("folder-index item must highlight as active, got %q", active)
|
||||||
|
}
|
||||||
|
if !strings.Contains(docsNavClass("/docs/user/filters/index", "/docs/user/filters", "pl-9"), "bg-brand/15") {
|
||||||
|
t.Errorf("normalization is symmetric (either URL order highlights)")
|
||||||
|
}
|
||||||
|
if !strings.Contains(docsNavClass("/docs/user/filters/cookbook", "/docs/user/filters/cookbook", "pl-9"), "bg-brand/15") {
|
||||||
|
t.Error("file-page highlight regression")
|
||||||
|
}
|
||||||
|
if strings.Contains(docsNavClass("/docs/user/filters/cookbook", "/docs/user/filters/index", "pl-9"), "bg-brand/15") {
|
||||||
|
t.Error("cookbook URL wrongly highlights the Overview item")
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-2
@@ -26,10 +26,15 @@ func activeClass(current, target string) string {
|
|||||||
|
|
||||||
// docsNavClass returns classes for docs sidebar links (server-rendered active
|
// docsNavClass returns classes for docs sidebar links (server-rendered active
|
||||||
// state — there is no client-side highlighter). indent sets the left padding,
|
// state — there is no client-side highlighter). indent sets the left padding,
|
||||||
// deeper for subcategory group items.
|
// deeper for subcategory group items. Folder-index pages compare normalized
|
||||||
|
// (their serving URL lacks the /index suffix their nav entry carries).
|
||||||
func docsNavClass(current, target, indent string) string {
|
func docsNavClass(current, target, indent string) string {
|
||||||
base := "nav-item flex items-center " + indent + " pr-4 mr-2 py-1.5 rounded-lg text-sm transition-colors "
|
base := "nav-item flex items-center " + indent + " pr-4 mr-2 py-1.5 rounded-lg text-sm transition-colors "
|
||||||
if strings.TrimRight(current, "/") == strings.TrimRight(target, "/") {
|
strip := func(u string) string {
|
||||||
|
u = strings.TrimRight(u, "/")
|
||||||
|
return strings.TrimSuffix(u, "/index")
|
||||||
|
}
|
||||||
|
if strip(current) == strip(target) {
|
||||||
return base + "bg-brand/15 text-brand font-medium"
|
return base + "bg-brand/15 text-brand font-medium"
|
||||||
}
|
}
|
||||||
return base + "text-content-muted hover:text-content hover:bg-surface-hover"
|
return base + "text-content-muted hover:text-content hover:bg-surface-hover"
|
||||||
|
|||||||
Reference in new issue
Block a user