feat(docs): standalone docs site chrome + category/subcategory navigation
Docs pages no longer embed the app Header (whose hx-post login form silently degraded to a native GET, leaking credentials into the URL — this page never loaded htmx). New docs-owned topbar: search, theme picker, Open App; anonymous visitors get a plain /login link. Optional-auth middleware (hand-rolled — echojwt's ErrorHandler return ends the chain, producing empty 200s) populates the user for signed-in visitors. Navigation rebuilt from the docs directory tree: 6 sections, subcategory groups, server-rendered expansion and active states. Search results are a templ partial (/docs/search/partial); code highlighting is server-side chroma (previously dead hljs CSS); prose is theme-aware via typography DEFAULT vars (prose-invert painted unreadable text under light themes). /docs renders the index directly; standalone 404 without the tailwind CDN. Tests: navigation routing/expansion (internal/docs), render pins asserting no login form/password field/app sidebar ever return on docs pages. lint-rendering: exempt *.test.ts fixtures (jsdom DOM building); docs.ts baseline 5 -> 1.
This commit is contained in:
1 parent
9d0ae2bcc4
commit
84cd6328f4
19 files changed
+2030
-1097
No files matched your search
@@ -0,0 +1,146 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The docs layout is a standalone documentation site: it must NOT embed the
|
||||
// app chrome (its sidebar assumed a signed-in user) and must never contain a
|
||||
// login form — the old layout posted credentials via htmx that was never
|
||||
// loaded on this page, degrading to a native GET that put the username and
|
||||
// password into the URL. These render tests pin that fix.
|
||||
func TestDocsLayoutRendersStandalone(t *testing.T) {
|
||||
nav := Navigation{
|
||||
Sections: []NavSection{
|
||||
{
|
||||
Title: "Getting Started",
|
||||
Icon: "book-open",
|
||||
Items: []NavItem{{Title: "Overview", URL: "/docs"}},
|
||||
},
|
||||
{
|
||||
Title: "User Guide",
|
||||
Icon: "book",
|
||||
Collapsed: true,
|
||||
Items: []NavItem{{Title: "Dashboard", URL: "/docs/user/dashboard"}},
|
||||
Groups: []NavGroup{
|
||||
{
|
||||
Title: "Devices",
|
||||
Items: []NavItem{{Title: "Kobo Setup", URL: "/docs/user/devices/kobo-setup"}},
|
||||
Collapsed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
doc := Document{
|
||||
Title: "Test Doc",
|
||||
Content: "<p>Hello docs</p>",
|
||||
TOC: []TOCItem{{Level: 2, Title: "Section", Anchor: "section"}},
|
||||
Breadcrumb: []BreadcrumbItem{
|
||||
{Title: "Docs", URL: "/docs"},
|
||||
{Title: "User Guide"},
|
||||
},
|
||||
SourceFile: "user/test.md",
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
if err := DocsLayout(nav, doc, User{Theme: "tokyo-night"}, "/docs/user/test").Render(context.Background(), &b); err != nil {
|
||||
t.Fatalf("DocsLayout: %v", err)
|
||||
}
|
||||
out := b.String()
|
||||
|
||||
for _, banned := range []string{
|
||||
"app-sidebar", // app chrome
|
||||
`hx-post="/api/auth/login"`, // no inline login form (the URL-leak bug)
|
||||
`name="password"`, // credentials never render on docs pages
|
||||
"prose-invert", // theme-aware prose, not hardcoded dark
|
||||
"highlight-dark.min.css", // highlighting is server-side now
|
||||
"cdn.tailwindcss.com", // no CDN dependencies
|
||||
"lunr", // dead client-side search is gone
|
||||
} {
|
||||
if strings.Contains(out, banned) {
|
||||
t.Errorf("docs layout must not contain %q", banned)
|
||||
}
|
||||
}
|
||||
|
||||
for _, want := range []string{
|
||||
`href="/docs"`, // sidebar + logo links
|
||||
`data-collapsed="true"`, // collapsed section state
|
||||
`data-collapsed="false"`, // expanded section state
|
||||
"Open App", // topbar action
|
||||
"theme-select", // theme picker
|
||||
"docs-search-results", // search dropdown target
|
||||
"Hello docs", // content rendered
|
||||
"page-docs",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("docs layout missing %q", want)
|
||||
}
|
||||
}
|
||||
|
||||
// Anonymous visitor: a sign-in LINK, never a form.
|
||||
b.Reset()
|
||||
if err := DocsLayout(nav, doc, User{Theme: "tokyo-night"}, "/docs/user/test").Render(context.Background(), &b); err != nil {
|
||||
t.Fatalf("DocsLayout anonymous: %v", err)
|
||||
}
|
||||
if !strings.Contains(b.String(), `href="/login"`) {
|
||||
t.Error("anonymous docs layout should link to /login")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsTopbarSignedIn(t *testing.T) {
|
||||
var b strings.Builder
|
||||
if err := DocsTopbar(User{ID: "u1", Username: "reader", Theme: "tokyo-night"}).Render(context.Background(), &b); err != nil {
|
||||
t.Fatalf("DocsTopbar: %v", err)
|
||||
}
|
||||
out := b.String()
|
||||
if strings.Contains(out, `href="/login"`) {
|
||||
t.Error("signed-in topbar must not offer /login")
|
||||
}
|
||||
if !strings.Contains(out, "reader") {
|
||||
t.Error("signed-in topbar should surface the username")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsSearchResultsPartial(t *testing.T) {
|
||||
var b strings.Builder
|
||||
results := []SearchResultItem{
|
||||
{Title: "Kobo Setup", Category: "User Guide", URL: "/docs/user/devices/kobo-setup"},
|
||||
}
|
||||
if err := DocsSearchResults(results).Render(context.Background(), &b); err != nil {
|
||||
t.Fatalf("DocsSearchResults: %v", err)
|
||||
}
|
||||
out := b.String()
|
||||
for _, want := range []string{"Kobo Setup", "User Guide", `/docs/user/devices/kobo-setup`} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("search results missing %q", want)
|
||||
}
|
||||
}
|
||||
|
||||
// Empty state renders, not blank markup.
|
||||
b.Reset()
|
||||
if err := DocsSearchResults(nil).Render(context.Background(), &b); err != nil {
|
||||
t.Fatalf("DocsSearchResults empty: %v", err)
|
||||
}
|
||||
if !strings.Contains(b.String(), "No matching pages.") {
|
||||
t.Error("search results empty state missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocsNotFoundRenders(t *testing.T) {
|
||||
var b strings.Builder
|
||||
if err := DocsNotFound("Document Not Found", "no such page").Render(context.Background(), &b); err != nil {
|
||||
t.Fatalf("DocsNotFound: %v", err)
|
||||
}
|
||||
out := b.String()
|
||||
for _, want := range []string{"Document Not Found", "no such page", `href="/docs"`} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("404 page missing %q", want)
|
||||
}
|
||||
}
|
||||
if strings.Contains(out, "cdn.tailwindcss.com") {
|
||||
t.Error("404 page must not pull the tailwind CDN")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user