docs(ui): cookie-rendered sidebar state, data-collapsed CSS, working overview links + link-checker gate

Three problems, one root cause each:

1. Sidebar jump: the server rendered its own default (current section
   open, rest collapsed) and restoreDocsNav corrected it after paint —
   open-then-close. Fix: persistence moved server-side. toggleCollapsible
   now writes a docs_nav cookie (strictly-necessary UI state, no
   identifiers, SameSite=Lax); ApplyNavOverrides merges it into the nav
   before render, so first paint IS the visitor's state. The post-paint
   restore is deleted.

2. Arrows out of sync / double-click: two visibility mechanisms
   disagreed. There was NO data-collapsed CSS at all — collapsed sections
   were only ever hidden by JS after paint, and the arrow rotation came
   from a different (server) state. Now: style.css hides on
   [data-collapsed=true] (single source of truth, server-rendered),
   applyCollapsed sets only the attribute, arrows rotate from the same
   merged value. Live check: 37/37 collapsibles arrow-consistent.

3. api-reference.md links: all 20+ quick links were directory-style
   (authentication/ etc.) that 404 on the docs site, plus stale targets
   (books/, ../collections-api.md, websocket 'coming soon'). Rewritten to
   real per-file links — all 9 unique targets return 200 live. Same rot
   fixed in admin/hash-conflicts.md, development.md, android-app.md,
   create_media_item.md, koreader-protocol.md; docs/index.md redirected
   from the deleted collections-api monolith (stale 530-line duplicate of
   the gated api/collections docs — deleted, per the WO2 doctrine).

New gate: TestDocsRelativeLinks walks docs/developer/** and fails make
test on any relative link that is directory-style or unresolvable —
overview links can never silently rot again.
This commit is contained in:
John O'Keefe committed 2026-10-24 20:20:07 -04:00
1 parent f1a6de7cd7
commit a8f8c4def9
17 files changed
+241 -674

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
package docs
import (
"os"
"path/filepath"
"regexp"
"strings"
"testing"
)
// TestDocsRelativeLinks keeps the developer docs tree internally navigable:
// every relative markdown link must point at a file that exists. The
// api-reference overview once shipped 20+ directory-style links
// (authentication/ etc.) that 404'd — this gate makes that class of rot
// fail make test. (docs/user is excluded: end-user prose has its own pass.)
//
// Rules:
// - http(s):, mailto:, and pure #anchors are skipped
// - every other target must end in .md and resolve (relative to the
// linking file) to an existing file inside the tree
// - directory-style targets ("area/") are the exact bug this test exists
// for and always fail
var docsLinkRe = regexp.MustCompile(`\[[^\]]*\]\(([^)\s]+)\)`)
func TestDocsRelativeLinks(t *testing.T) {
// Walk developer docs; escape-check against the whole docs tree —
// cross-area links into docs/user are legitimate (same site).
devRoot, err := filepath.Abs("../../docs/developer")
if err != nil {
t.Fatalf("abs developer root: %v", err)
}
treeRoot, err := filepath.Abs("../../docs")
if err != nil {
t.Fatalf("abs docs root: %v", err)
}
err = filepath.WalkDir(devRoot, func(path string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() || !strings.HasSuffix(path, ".md") {
return err
}
raw, err := os.ReadFile(path)
if err != nil {
return err
}
rel, _ := filepath.Rel(docsRoot, path)
absPath, err := filepath.Abs(path)
if err != nil {
return err
}
for _, m := range docsLinkRe.FindAllStringSubmatch(string(raw), -1) {
target := m[1]
if strings.HasPrefix(target, "http://") ||
strings.HasPrefix(target, "https://") ||
strings.HasPrefix(target, "mailto:") {
continue
}
if strings.HasPrefix(target, "#") {
continue // in-page anchor
}
// strip a trailing fragment for file resolution
fileTarget := target
if i := strings.Index(fileTarget, "#"); i >= 0 {
fileTarget = fileTarget[:i]
}
if fileTarget == "" {
continue // pure-anchor form already handled; empty = odd, skip
}
if !strings.HasSuffix(fileTarget, ".md") {
t.Errorf("%s: link target %q is not a .md file — directory-style links 404 on the docs site; link the file (e.g. %q)",
rel, target, strings.TrimSuffix(fileTarget, "/")+".md")
continue
}
resolved := filepath.Clean(filepath.Join(filepath.Dir(absPath), fileTarget))
if !strings.HasPrefix(resolved, treeRoot+string(filepath.Separator)) {
t.Errorf("%s: link target %q escapes the docs tree", rel, target)
continue
}
if _, err := os.Stat(resolved); err != nil {
t.Errorf("%s: link target %q does not resolve to an existing file (%s)", rel, target, resolved)
}
}
return nil
})
if err != nil {
t.Fatalf("walk docs: %v", err)
}
}