docs nav: promote Devices and Administration out of User Guide

Follows the developer/api → API Reference carve-out: placement-only, no
file paths or URLs change. placeDoc routes user/devices/* to a new
top-level "Devices" section (icon: device) and user/admin-* to a new
top-level "Administration" section (icon: shield); User Guide keeps its
direct guides with no groups. Sidebar order: Getting Started, User
Guide, Devices, Administration, API Reference, Developer, Operations,
Contributing.

Verified live: on kobo-setup the Devices section opens with the three
device guides; on admin-guide Administration opens with both admin
guides; on user/dashboard User Guide opens as before. Tests
(TestPlaceDoc, TestBuildNavigation) updated to pin the new placement.
This commit is contained in:
John O'Keefe committed 2026-10-25 21:10:53 -04:00
1 parent fb95b6d400
commit 27f42bf703
2 files changed
+28 -24

No files matched your search

+8 -4
View File
@@ -20,6 +20,8 @@ var sectionOrder = []struct {
}{ }{
{Title: "Getting Started", Icon: "book-open"}, {Title: "Getting Started", Icon: "book-open"},
{Title: "User Guide", Icon: "book"}, {Title: "User Guide", Icon: "book"},
{Title: "Devices", Icon: "device"},
{Title: "Administration", Icon: "shield"},
{Title: "API Reference", Icon: "list"}, {Title: "API Reference", Icon: "list"},
{Title: "Developer", Icon: "gear"}, {Title: "Developer", Icon: "gear"},
{Title: "Operations", Icon: "database"}, {Title: "Operations", Icon: "database"},
@@ -90,18 +92,20 @@ func placeDoc(docPath string) navPlacement {
case strings.HasPrefix(docPath, "user/"): case strings.HasPrefix(docPath, "user/"):
rest := strings.TrimPrefix(docPath, "user/") rest := strings.TrimPrefix(docPath, "user/")
if dir, _, ok := strings.Cut(rest, "/"); ok { if dir, _, ok := strings.Cut(rest, "/"); ok {
// Subdirectories become subcategories: device setup gets its // Device setup guides get their own top-level section (the
// own group, the admin-facing guides another. // directory stays where it is — placement is nav-only, same
// carve-out as developer/api → API Reference).
if dir == "devices" { if dir == "devices" {
return navPlacement{section: "User Guide", group: "Devices", pin: pin} return navPlacement{section: "Devices", pin: pin}
} }
return navPlacement{section: "User Guide", group: titleCase(dir), pin: pin} return navPlacement{section: "User Guide", group: titleCase(dir), pin: pin}
} }
if rest == "user-guide.md" { if rest == "user-guide.md" {
return navPlacement{section: "User Guide", pin: -50} return navPlacement{section: "User Guide", pin: -50}
} }
// Admin-facing guides are their own top-level section too.
if strings.HasPrefix(rest, "admin-") { if strings.HasPrefix(rest, "admin-") {
return navPlacement{section: "User Guide", group: "Administration"} return navPlacement{section: "Administration"}
} }
return navPlacement{section: "User Guide"} return navPlacement{section: "User Guide"}
+20 -20
View File
@@ -4,8 +4,6 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"bookhoard/templates"
) )
func TestPlaceDoc(t *testing.T) { func TestPlaceDoc(t *testing.T) {
@@ -18,8 +16,8 @@ func TestPlaceDoc(t *testing.T) {
{"index.md", "Getting Started", "", true}, {"index.md", "Getting Started", "", true},
{"user/user-guide.md", "User Guide", "", true}, {"user/user-guide.md", "User Guide", "", true},
{"user/dashboard.md", "User Guide", "", false}, {"user/dashboard.md", "User Guide", "", false},
{"user/admin-guide.md", "User Guide", "Administration", false}, {"user/admin-guide.md", "Administration", "", false},
{"user/devices/kobo-setup.md", "User Guide", "Devices", false}, {"user/devices/kobo-setup.md", "Devices", "", false},
{"developer/api/api-reference.md", "API Reference", "", true}, {"developer/api/api-reference.md", "API Reference", "", true},
{"developer/api/dashboard.md", "API Reference", "", false}, {"developer/api/dashboard.md", "API Reference", "", false},
{"developer/api/authentication/login.md", "API Reference", "Authentication", false}, {"developer/api/authentication/login.md", "API Reference", "Authentication", false},
@@ -119,7 +117,7 @@ func TestBuildNavigation(t *testing.T) {
t.Fatal("no sections built") t.Fatal("no sections built")
} }
wantOrder := []string{"Getting Started", "User Guide", "API Reference", "Developer", "Operations", "Contributing"} wantOrder := []string{"Getting Started", "User Guide", "Devices", "Administration", "API Reference", "Developer", "Operations", "Contributing"}
for i, want := range wantOrder { for i, want := range wantOrder {
if nav.Sections[i].Title != want { if nav.Sections[i].Title != want {
t.Fatalf("section %d = %q, want %q", i, nav.Sections[i].Title, want) t.Fatalf("section %d = %q, want %q", i, nav.Sections[i].Title, want)
@@ -134,38 +132,40 @@ func TestBuildNavigation(t *testing.T) {
t.Errorf("Getting Started items = %+v, want single Overview → /docs", getting.Items) t.Errorf("Getting Started items = %+v, want single Overview → /docs", getting.Items)
} }
// User Guide no longer contains the current page (device guides are
// their own section now): collapsed, no groups.
userGuide := nav.Sections[1] userGuide := nav.Sections[1]
if userGuide.Collapsed { if !userGuide.Collapsed {
t.Error("User Guide should be expanded: it contains the current page") t.Error("User Guide should be collapsed: it no longer contains the current page")
}
if len(userGuide.Groups) != 0 {
t.Errorf("User Guide groups = %+v, want none (Devices + Administration carved out)", userGuide.Groups)
} }
if len(userGuide.Items) != 2 || userGuide.Items[0].Title != "User Guide" { if len(userGuide.Items) != 2 || userGuide.Items[0].Title != "User Guide" {
t.Errorf("User Guide direct items = %+v, want User Guide pinned first", userGuide.Items) t.Errorf("User Guide direct items = %+v, want User Guide pinned first", userGuide.Items)
} }
var devices *templates.NavGroup
for i := range userGuide.Groups { // Devices: own top-level section, expanded, no groups.
if userGuide.Groups[i].Title == "Devices" { devices := nav.Sections[2]
devices = &userGuide.Groups[i]
}
}
if devices == nil {
t.Fatal("User Guide has no Devices group")
}
if devices.Collapsed { if devices.Collapsed {
t.Error("Devices group should be expanded: it contains the current page") t.Error("Devices section should be expanded: it contains the current page")
}
if len(devices.Groups) != 0 {
t.Errorf("Devices groups = %+v, want none", devices.Groups)
} }
if len(devices.Items) != 2 || devices.Items[0].Title != "Kobo Setup" { if len(devices.Items) != 2 || devices.Items[0].Title != "Kobo Setup" {
t.Errorf("Devices items = %+v, want Kobo Setup + KOReader Setup", devices.Items) t.Errorf("Devices items = %+v, want Kobo Setup + KOReader Setup", devices.Items)
} }
// Everything else starts collapsed. // Everything else starts collapsed.
for _, s := range nav.Sections[2:] { for _, s := range nav.Sections[3:] {
if !s.Collapsed { if !s.Collapsed {
t.Errorf("section %q should be collapsed (current page is elsewhere)", s.Title) t.Errorf("section %q should be collapsed (current page is elsewhere)", s.Title)
} }
} }
// API Reference: subdirectory groups with the index page as Overview. // API Reference: subdirectory groups with the index page as Overview.
api := nav.Sections[2] api := nav.Sections[4]
if len(api.Groups) != 2 { if len(api.Groups) != 2 {
t.Fatalf("API Reference groups = %d, want 2 (Authentication, Saved Filters)", len(api.Groups)) t.Fatalf("API Reference groups = %d, want 2 (Authentication, Saved Filters)", len(api.Groups))
} }
@@ -181,7 +181,7 @@ func TestBuildNavigation(t *testing.T) {
} }
// Stray root files land in Developer → Reference. // Stray root files land in Developer → Reference.
dev := nav.Sections[3] dev := nav.Sections[5]
if len(dev.Groups) != 1 || dev.Groups[0].Title != "Reference" { if len(dev.Groups) != 1 || dev.Groups[0].Title != "Reference" {
t.Errorf("Developer groups = %+v, want Reference", dev.Groups) t.Errorf("Developer groups = %+v, want Reference", dev.Groups)
} }