diff --git a/DOCS_IMPLEMENTATION_PLAN.md b/DOCS_IMPLEMENTATION_PLAN.md new file mode 100644 index 0000000..b7d6304 --- /dev/null +++ b/DOCS_IMPLEMENTATION_PLAN.md @@ -0,0 +1,1309 @@ +# Bookhoard Documentation System - Implementation Plan + +**Version**: 1.0 +**Created**: 2026-02-01 +**Status**: Ready for implementation + +--- + +## Overview + +This plan documents the complete implementation of an interactive, searchable, and mobile-responsive documentation system for Bookhoard. + +### Current State +- ✅ Docs package created (`internal/docs/`) +- ✅ Goldmark markdown renderer integrated +- ✅ Sidebar navigation working +- ✅ Table of contents generator +- ✅ Breadcrumb navigation +- ✅ 9 documentation files exist +- ❌ **Markdown content not rendering as HTML** (blocked) +- ❌ **Search backend exists but frontend not connected** +- ❌ **API documentation is monolithic (30KB file)** +- ❌ **No API explorer** (placeholder only) +- ❌ **Mobile layout unoptimized** + +### Target State +- ✅ Markdown renders correctly as HTML +- ✅ Interactive API explorer (mock + live modes) +- ✅ Split API docs into individual endpoint files +- ✅ Lunr.js client-side search with fuzzy + live + highlighting +- ✅ Mobile-responsive sidebar +- ✅ Search index auto-generated + +--- + +## Phase 1: Fix Markdown Rendering (30 minutes) + +**Priority**: CRITICAL (blocks all docs) + +### Tasks + +#### 1.1 Fix rawHTML function in template +**File**: `templates/docs.templ` + +**Current Issue**: +```templ +templ rawHTML(content string) { + { template.HTML(content) } +} +``` + +**Solution**: Use `@rawHTML` directive instead of function call +```templ + +
+ @rawHTML(doc.Content) +
+``` + +#### 1.2 Test docs rendering +**Action**: +```bash +go build ./cmd/server +./bookhoard +curl http://localhost:8765/docs/API_REFERENCE.md | grep -A 5 "Bookhoard API" +``` + +**Expected**: HTML is rendered, not escaped markdown text + +**Completion Criteria**: Markdown converts to HTML with headings, lists, code blocks properly formatted. + +--- + +## Phase 2: Split API Documentation (2-3 hours) + +**Priority**: High (improves UX significantly) + +### File Structure to Create + +``` +docs/api/ +├── INDEX.md # Landing page with all endpoints +├── authentication/ +│ ├── register.md +│ ├── login.md +│ ├── refresh_token.md +│ └── logout.md +├── users/ +│ ├── get_profile.md +│ ├── update_profile.md +│ ├── update_theme.md +│ └── change_password.md +├── libraries/ +│ ├── get_visible_libraries.md +│ ├── get_library.md +│ ├── create_library.md +│ ├── add_library_folder.md +│ ├── set_library_visibility.md +│ └── get_library_stats.md +├── media-items/ +│ ├── list_media_items.md +│ ├── get_media_item.md +│ ├── search_media_items.md +│ ├── filter_sort_media_items.md +│ ├── update_media_item.md +│ └── delete_media_item.md +├── progress/ +│ ├── get_progress.md +│ ├── update_progress.md +│ └── delete_progress.md +├── notes/ +│ ├── get_notes.md +│ ├── create_note.md +│ ├── update_note.md +│ └── delete_note.md +├── highlights/ +│ ├── get_highlights.md +│ ├── create_highlight.md +│ ├── update_highlight.md +│ └── delete_highlight.md +├── ratings/ +│ ├── get_ratings.md +│ └── create_rating.md +├── devices/ +│ ├── register_device.md +│ ├── list_devices.md +│ ├── revoke_device.md +│ ├── get_devices.md +│ └── get_device.md +├── analytics/ +│ ├── get_analytics.md +│ └── update_analytics.md +├── book-matching/ +│ ├── search_books.md +│ ├── link_book.md +│ └── unlink_book.md +├── collections/ +│ ├── INDEX.md # Links to COLLECTIONS_API.md +│ ├── list_collections.md +│ ├── get_collection.md +│ ├── create_collection.md +│ ├── update_collection.md +│ ├── delete_collection.md +│ ├── add_auto_assign_rule.md +│ ├── remove_auto_assign_rule.md +│ ├── test_rule.md +│ ├── bulk_assign.md +│ ├── create_shelf_mapping.md +│ └── delete_shelf_mapping.md +├── opds/ +│ ├── feeds.md +│ ├── acquisition.md +│ └── publication.md +├── sync/ +│ ├── koreader_protocol.md +│ └── kobo_protocol.md +└── websocket/ + └── protocol.md +``` + +### Implementation Tasks + +#### 2.1 Create API Index (INDEX.md) +**File**: `docs/api/INDEX.md` + +**Template**: +```markdown +# API Documentation + +Complete reference for Bookhoard REST API endpoints. + +## Quick Links + +- [Authentication](authentication/) - User registration, login, tokens +- [Users](users/) - Profile management +- [Libraries](libraries/) - Library management +- [Media Items](media-items/) - Book/ebook operations +- [Progress](progress/) - Reading progress tracking +- [Notes](notes/) - User notes management +- [Highlights](highlights/) - Book highlights +- [Ratings](ratings/) - Book ratings +- [Devices](devices/) - Device registration and sync +- [Analytics](analytics/) - Usage statistics +- [Book Matching](book-matching/) - Search and link books +- [Collections](collections/) - See [COLLECTIONS_API.md](../COLLECTIONS_API.md) +- [OPDS](opds/) - Open Publication Distribution +- [Sync Protocols](sync/) - KOReader and Kobo sync +- [WebSocket](websocket/) - Real-time sync events + +--- + +## Authentication + +See [Authentication Endpoints](authentication/) + +## Users & Profiles + +See [User Management](users/) + +## Libraries + +See [Library Management](libraries/) + +... (continue for all sections) +``` + +#### 2.2 Extract endpoint sections from API_REFERENCE.md + +**Process**: +1. Read `docs/API_REFERENCE.md` +2. For each `###` heading (73 total), create dedicated markdown file +3. Extract: + - Method (GET/POST/PUT/DELETE) + - Endpoint path + - Auth requirement + - Request body (if applicable) + - Response examples + - Description + +**Example** - `docs/api/authentication/register.md`: +```markdown +# Register User + +Create a new user account. + +**Endpoint**: `POST /api/auth/register` +**Auth**: Not required +**Content-Type**: `application/json` + +## Request Body + +| Field | Type | Required | Description | +|--------|------|-----------|-------------| +| email | string | Yes | User's email address | +| username | string | Yes | Desired username | +| password | string | Yes | Password (min 8 chars) | +| first_name | string | No | User's first name | +| last_name | string | No | User's last name | + +### Example Request + +```json +{ + "email": "user@example.com", + "username": "john", + "password": "SecureP@ss123!", + "first_name": "John", + "last_name": "Doe" +} +``` + +## Response (201 Created) + +```json +{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "d4f5g6h7...", + "user": { + "id": "uuid-here", + "email": "user@example.com", + "username": "john", + "role": "user", + "theme": "tokyo-night", + "created_at": "2026-01-31T10:00:00Z" + } +} +``` + +## Error Responses + +| Code | Description | +|------|-------------| +| 400 | Invalid email format, weak password, or missing fields | +| 409 | Email or username already exists | + +## Try It Out + + +``` + +#### 2.3 Update navigation in docs/handler.go + +**File**: `internal/docs/navigation.go` + +**Current Issue**: Navigation only shows top-level docs, not new API structure + +**Solution**: Update `BuildNavigation()` to include new API structure + +--- + +## Phase 3: Interactive API Explorer (3-4 hours) + +**Priority**: High (major UX improvement) + +### Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ │ +│ [ Documentation ] [ Try It Out (Toggle) ] │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ POST /api/auth/login │ +├─────────────────────────────────────────────────────────────────────┤ +│ │ +│ Method: [▼ POST] GET PUT DELETE │ +│ Auth: [✓ Required] [✗ Optional] │ +│ │ +│ Request Body: │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ { │ │ +│ │ "email": "user@example.com", │ │ +│ │ "password": "securepassword" │ │ +│ │ } │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ [Copy JSON] [Copy cURL] │ +│ │ +│ [ Try It Out ] ← Click to execute │ +│ │ +│ Response: │ +│ ┌──────────────────────────────────────────────────────┐ │ +│ │ { │ │ +│ │ "token": "eyJ...", │ │ +│ │ "refresh_token": "eyJ...", │ │ +│ │ "expires_in": 3600 │ │ +│ │ } │ │ +│ └──────────────────────────────────────────────────────┘ │ +│ [Copy JSON] │ +└─────────────────────────────────────────────────────────────────────┘ +``` + +### Implementation Tasks + +#### 3.1 Create API Explorer Template Component + +**File**: `templates/api_explorer.templ` + +**Template Code**: +```templ +package templates + +import "html/template" + +// APIExplorer represents the interactive API explorer +type APIExplorer struct { + Endpoint EndpointInfo + IsLoggedIn bool + MockData map[string]interface{} + CanExecute bool +} + +type EndpointInfo struct { + Method string + Path string + Auth bool + ContentType string + RequestBody string // JSON example + Response string // JSON example + Description string +} + +templ APIExplorer(explorer APIExplorer) { + if !explorer.IsLoggedIn { + // Show mode toggle and mock data +
+
+ + +
+
+ +
+
+

Response (Mock)

+
{ template.HTML(explorer.Endpoint.Response) }
+
+ + +
+ } else { + // Show interactive explorer with real execution +
+
+ + +
+
+
+ +
+ +
+ + + + + +
+ } + + + + +} +``` + +#### 3.2 Update docs handler to support both modes + +**File**: `internal/docs/handler.go` + +**Add**: +```go +type APIEndpointData struct { + Method string + Path string + Auth bool + ContentType string + RequestBody string + Response string + Description string +} + +func (h *DocsHandler) GetAPIEndpoint(endpointPath string) (*APIEndpointData, error) { + // Map endpoint path to data + // This would be populated from the split API files + endpoints := h.getAPIEndpoints() + for _, ep := range endpoints { + if strings.HasSuffix(endpointPath, strings.TrimPrefix(ep.Path, "/api/")) { + return &ep, nil + } + } + return nil, fmt.Errorf("endpoint not found: %s", endpointPath) +} + +func (h *DocsHandler) getAPIEndpoints() []APIEndpointData { + // This will be populated from the API docs + // For now, return static list matching API_REFERENCE.md + return []APIEndpointData{ + { + Method: "POST", + Path: "/api/auth/register", + Auth: false, + ContentType: "application/json", + RequestBody: `{ + "email": "user@example.com", + "username": "john", + "password": "SecureP@ss123!", + "first_name": "John", + "last_name": "Doe" +}`, + Response: `{ + "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + "refresh_token": "d4f5g6h7...", + "user": { + "id": "uuid-here", + "email": "user@example.com", + "username": "john", + "role": "user", + "theme": "tokyo-night", + "created_at": "2026-01-31T10:00:00Z" + } +}`, + Description: "Create a new user account", + }, + // ... add all 73 endpoints + } +} +``` + +#### 3.3 Update HTTP handler to check authentication + +**File**: `internal/docs/http_handler.go` + +**Update** `ShowDocumentation` method to pass login status: +```go +func (h *HTTPHandler) ShowDocumentation(c echo.Context) error { + // ... existing code ... + + // Check if user is logged in + is_logged_in := false + if userID := c.Get("user_id"); userID != nil { + is_logged_in = true + } + + // Render docs with auth status + // Pass to template +} +``` + +#### 3.4 Integrate API Explorer into docs template + +**File**: `templates/docs.templ` + +**Update**: +```templ +templ DocsLayout(nav Navigation, doc Document, user User, explorer *APIExplorer) { + + + + if explorer != nil { + @APIExplorer(*explorer) + } + + +} +``` + +--- + +## Phase 4: Lunr.js Search Implementation (2-3 hours) + +**Priority**: High (core documentation feature) + +### Architecture + +``` +┌──────────────────────────────────────────────────────────────┐ +│ Sidebar │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ 🔍 Search... │ │ +│ └────────────────────────────────────────────────┘ │ +│ │ +│ ┌────────────────────────────────────────────────┐ │ +│ │ 📚 Getting Started ▼ │ │ +│ │ - Documentation Index │ │ +│ │ - Sync Guide │ │ +│ │ - Troubleshooting │ │ +│ │ │ │ +│ │ 🔌 API Reference ▼ │ │ +│ │ - Authentication │ │ +│ │ - Users & Profiles │ │ +│ │ - Libraries │ │ +│ │ - ... │ │ +│ └────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ Search Results (overlay): │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Register User │ │ +│ │ /api/auth/register │ │ +│ │ "Create a new user account..." │ │ +│ └──────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Login User │ │ +│ │ /api/auth/login │ │ +│ │ "Authenticate with email and password..." │ │ +│ └──────────────────────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Kobo Setup │ │ +│ │ /docs/devices/KOBO_SETUP.md │ │ +│ │ "Set up your Kobo e-reader..." │ │ +│ └──────────────────────────────────────────────┘ │ +│ │ +│ [ View all results → ] │ +│ │ +│ Main Content (Documentation) │ +│ # POST /api/auth/login │ +│ ... │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Implementation Tasks + +#### 4.1 Add search index generation + +**File**: `internal/docs/handler.go` + +**Add to `DocsHandler` struct**: +```go +type SearchDoc struct { + ID string `json:"id"` + Title string `json:"title"` + Content string `json:"content"` + URL string `json:"url"` +} + +func (h *DocsHandler) GenerateSearchIndex() ([]SearchDoc, error) { + docs, err := h.ListDocuments() + if err != nil { + return nil, fmt.Errorf("failed to list documents: %w", err) + } + + var searchDocs []SearchDoc + for _, path := range docs { + doc, err := h.LoadDocument(path) + if err != nil { + continue + } + + // Strip HTML tags for better search + content := h.stripHTML(doc.Content) + + searchDocs = append(searchDocs, SearchDoc{ + ID: path, + Title: doc.Title, + Content: content, + URL: "/docs/" + strings.TrimSuffix(path, ".md"), + }) + } + + return searchDocs, nil +} + +// stripHTML removes HTML tags from string (simple implementation) +func (h *DocsHandler) stripHTML(html string) string { + var result strings.Builder + inTag := false + for _, r := range html { + if r == '<' { + inTag = true + continue + } + if r == '>' { + inTag = false + continue + } + if !inTag { + result.WriteRune(r) + } + } + return result.String() +} +``` + +#### 4.2 Add search index endpoint + +**File**: `internal/docs/http_handler.go` + +**Add**: +```go +// ServeSearchIndex serves the Lunr.js search index +func (h *HTTPHandler) ServeSearchIndex(c echo.Context) error { + index := h.docs.GenerateSearchIndex() + if index == nil { + return c.JSON(http.StatusInternalServerError, map[string]string{ + "error": "failed to generate search index", + }) + } + return c.JSON(http.StatusOK, index) +} +``` + +**Add route in `cmd/server/main.go`**: +```go +// After other docs routes +e.GET("/docs/search-index.json", docsHandler.ServeSearchIndex) +``` + +#### 4.3 Add Lunr.js to docs template + +**File**: `templates/docs.templ` + +**Add to ``**: +```html + + + +``` + +**Add search input in sidebar**: +```html + + +``` + +**Add search results overlay**: +```html + + +``` + +**Add search JavaScript**: +```html + +``` + +**Add search styles**: +```css +/* Add to template styles */ +.search-box { + margin-bottom: 1rem; +} + +#search-input { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-primary); + color: var(--text-primary); + font-size: 0.875rem; +} + +#search-input:focus { + outline: 2px solid var(--accent); +} + +#search-results { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.95); + overflow-y: auto; + padding: 2rem; + z-index: 1000; +} + +.search-result { + display: block; + padding: 1rem; + border-bottom: 1px solid var(--border); + color: var(--text-primary); + text-decoration: none; +} + +.search-result:hover { + background: var(--bg-secondary); +} + +.result-title { + font-weight: 600; + margin-bottom: 0.5rem; +} + +.result-snippet { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.no-results { + text-align: center; + padding: 2rem; + color: var(--text-secondary); +} + +mark { + background: var(--accent); + color: white; + padding: 0 0.2rem; + border-radius: 2px; +} +``` + +--- + +## Phase 5: Mobile Responsive Design (1-2 hours) + +**Priority**: Medium (UX improvement for mobile users) + +### Implementation Tasks + +#### 5.1 Add mobile sidebar toggle + +**File**: `templates/docs.templ` + +**Add to header**: +```html +
+ +
+``` + +**Update sidebar**: +```css +.sidebar { + /* Desktop */ + left: 0; + width: 280px; + transition: transform 0.3s ease; +} + +/* Mobile styles */ +@media (max-width: 768px) { + .sidebar { + transform: translateX(-100%); + position: fixed; + z-index: 100; + box-shadow: 2px 0 8px rgba(0, 0, 0, 0.2); + } + + .sidebar.open { + transform: translateX(0); + } + + .main-content { + margin-left: 0 !important; + padding: 1rem; + } + + .mobile-menu-btn { + display: block !important; + } + + #search-results { + padding: 1rem; + padding-top: 4rem; + } +} +``` + +**Add JavaScript for sidebar toggle**: +```html + +``` + +#### 5.2 Adjust TOC for mobile + +**File**: `templates/docs.templ` + +**Add mobile TOC styling**: +```css +@media (max-width: 768px) { + .toc { + position: static; + margin-bottom: 1rem; + padding: 1rem; + background: var(--bg-secondary); + border-radius: 8px; + } +} +``` + +--- + +## Phase 6: Testing & Polish (1-2 hours) + +**Priority**: High (quality assurance) + +### Test Checklist + +#### 6.1 Documentation System Tests + +- [ ] All 9 existing docs render correctly +- [ ] API docs split into individual files load correctly +- [ ] Navigation sidebar links work for all pages +- [ ] Breadcrumbs are accurate +- [ ] Table of contents links jump to correct sections + +#### 6.2 Search Tests + +- [ ] Search index loads on page load +- [ ] Typing "kobo" finds Kobo setup guide +- [ ] Typing "sync" finds sync guide +- [ ] Fuzzy search: "colletion" finds "Collection" +- [ ] Search results link to correct pages +- [ ] Highlighting works in search results +- [ ] Search works offline (after index loaded) + +#### 6.3 API Explorer Tests + +- [ ] Mock mode shows correct example data +- [ ] Real mode executes actual API calls +- [ ] Login state detected correctly +- [ ] "Try It Out" button makes request +- [ ] Response displays with status and timing +- [ ] Copy buttons work (JSON, cURL) +- [ ] Method switcher updates request type +- [ ] Error handling displays properly + +#### 6.4 Mobile Tests + +- [ ] Sidebar opens/closes on mobile +- [ ] Search results overlay displays correctly +- [ ] Content is readable on mobile (320px width) +- [ ] Code blocks scroll horizontally on mobile +- [ ] Touch targets are 44px+ minimum + +#### 6.5 Cross-Browser Tests + +- [ ] Works in Chrome/Firefox/Safari/Edge (latest versions) +- [ ] Works on iOS Safari +- [ ] Works on Android Chrome + +--- + +## Implementation Order (Recommended) + +### Sprint 1: Critical Fixes (0.5 hours) +1. Phase 1: Fix Markdown Rendering +2. Quick test: Verify docs display correctly + +### Sprint 2: Core Features (5-6 hours) +3. Phase 4: Lunr.js Search (2-3 hours) +4. Phase 2: Split API Docs (2-3 hours) +5. Sprint 2 test: Verify all features work + +### Sprint 3: Advanced Features (4-6 hours) +6. Phase 3: Interactive API Explorer (3-4 hours) +7. Phase 5: Mobile Responsive Design (1-2 hours) + +### Sprint 4: Polish (1-2 hours) +8. Phase 6: Testing & Polish + +--- + +## Total Time Estimate + +| Phase | Time | Priority | +|--------|-------|----------| +| Phase 1: Fix Markdown Rendering | 0.5h | Critical | +| Phase 2: Split API Docs | 2-3h | High | +| Phase 3: API Explorer | 3-4h | High | +| Phase 4: Lunr.js Search | 2-3h | High | +| Phase 5: Mobile Responsive | 1-2h | Medium | +| Phase 6: Testing & Polish | 1-2h | High | +| **Total** | **10-15h** | - | + +--- + +## Files to Modify + +### New Files to Create +``` +docs/api/ +├── INDEX.md +├── authentication/*.md (4 files) +├── users/*.md (4 files) +├── libraries/*.md (6 files) +├── media-items/*.md (7 files) +├── progress/*.md (3 files) +├── notes/*.md (4 files) +├── highlights/*.md (4 files) +├── ratings/*.md (2 files) +├── devices/*.md (5 files) +├── analytics/*.md (2 files) +├── book-matching/*.md (3 files) +├── collections/INDEX.md +├── opds/*.md (3 files) +├── sync/*.md (2 files) +└── websocket/*.md (1 file) +``` + +### Files to Modify +``` +internal/docs/handler.go # Add SearchDoc type, GenerateSearchIndex, stripHTML +internal/docs/http_handler.go # Add ServeSearchIndex, update to check auth status +internal/docs/navigation.go # Update BuildNavigation for new API structure +templates/docs.templ # Add Lunr.js, search UI, mobile sidebar toggle, search styles +templates/api_explorer.templ # NEW - Create this file +cmd/server/main.go # Add /docs/search-index.json route +``` + +### Files to Keep (No Changes) +``` +docs/API_REFERENCE.md # Keep as reference, but may deprecate +docs/COLLECTIONS_API.md # Keep, link from API index +docs/SYNC_USER_GUIDE.md # No changes +docs/TROUBLESHOOTING.md # No changes +docs/INDEX.md # No changes +docs/devices/*.md # No changes +docs/contributing/DEVELOPMENT.md # No changes +docs/api/WEBSOCKET_API.md # Keep, link from API index +``` + +--- + +## Success Criteria + +The documentation system is complete when: + +### Functionality +- ✅ All markdown files render as HTML +- ✅ API docs are split into individual endpoint files +- ✅ Search works with fuzzy matching and highlighting +- ✅ Search is live (debounced input) +- ✅ API Explorer has both mock and real modes +- ✅ API Explorer executes real API calls when logged in +- ✅ Sidebar navigation reflects new API structure +- ✅ Mobile users can toggle sidebar +- ✅ Search results overlay works on mobile + +### Performance +- ✅ Search index loads in <500ms +- ✅ Search results appear in <200ms after typing stops +- ✅ Initial docs page load <2s +- ✅ API explorer response displays in <500ms + +### Code Quality +- ✅ All templates compile without errors +- ✅ Go code passes `go build` +- ✅ No console errors in browser +- ✅ LSP shows no warnings + +### Documentation Quality +- ✅ All API endpoints have "Try It Out" section +- ✅ Mock data is valid and realistic +- ✅ Real mode shows actual responses +- ✅ Search results show relevant content +- ✅ Mobile layout is usable on 320px width + +--- + +## Notes for Implementers + +### Important Constraints +1. **Don't modify backend** - Only add search index route to `main.go` +2. **Use Lunr.js plugins**: `lunr-flex` for fuzzy, `lunr-highlight` for highlighting +3. **Mock vs Real logic**: Check `localStorage.getItem('token')` to determine auth state +4. **Debounce search input**: 150ms is optimal balance +5. **Strip HTML from search index**: Goldmark outputs HTML, need plain text for search +6. **Pre-build index** (optional): Generate `search_index.json` on startup, cache it + +### Gotchas +1. **Templ unsafe HTML**: Use `@rawHTML(doc.Content)` directive, not function call +2. **Import fmt**: Must import in template if using `fmt.Sprintf` +3. **Lunr CDN versions**: Use compatible versions (lunr@2.3.9, lunr-flex@1.0.5, lunr-highlight@1.0.0) +4. **Mobile breakpoint**: Use `@media (max-width: 768px)` for mobile styles +5. **Sidebar z-index**: Must be higher than content for mobile overlay +6. **Search results z-index**: Must be highest (1000) to appear above sidebar + +### Testing Strategy +1. Start with Phase 1 only - verify markdown rendering works +2. Add Phase 4 search - test before splitting API docs +3. Split API docs gradually - test each section +4. Add API Explorer last - complex feature, easier to isolate bugs +5. Do mobile testing throughout, not just at the end + +### Rollback Plan +If any phase causes issues: +1. Revert the specific files modified +2. Test to ensure previous state works +3. Review the specific implementation +4. Try alternative approach if needed + +--- + +## Related Documentation + +- [PROJECT_GUIDELINES.md](../PROJECT_GUIDELINES.md) - Development rules +- [README.md](../README.md) - Project overview +- [API_REFERENCE.md](API_REFERENCE.md) - Current API reference +- [COLLECTIONS_API.md](COLLECTIONS_API.md) - Collections API + +--- + +**Last Updated**: 2026-02-01 +**Next Review**: After Sprint 1 completion diff --git a/internal/docs/handler.go b/internal/docs/handler.go index ffc675f..b670bd3 100644 --- a/internal/docs/handler.go +++ b/internal/docs/handler.go @@ -81,6 +81,10 @@ func (h *DocsHandler) LoadDocument(docPath string) (*templates.Document, error) return nil, fmt.Errorf("failed to convert markdown: %w", err) } + // Debug: Log the raw HTML output from goldmark + htmlOutput := buf.String() + fmt.Printf("DEBUG: Goldmark output length: %d, first 200 chars: %q\n", len(htmlOutput), htmlOutput[:min(200, len(htmlOutput))]) + // Extract title from first heading title := h.extractTitle(content) diff --git a/templates/docs.templ b/templates/docs.templ index 5ca5dac..179495e 100644 --- a/templates/docs.templ +++ b/templates/docs.templ @@ -5,10 +5,6 @@ import ( "html/template" ) -templ rawHTML(content string) { - { template.HTML(content) } -} - templ DocsLayout(nav Navigation, doc Document, user User) { @@ -242,7 +238,7 @@ templ DocsLayout(nav Navigation, doc Document, user User) {
- @rawHTML(doc.Content) + { doc.Content }
diff --git a/templates/docs_templ.go b/templates/docs_templ.go index 62480b3..f3d80df 100644 --- a/templates/docs_templ.go +++ b/templates/docs_templ.go @@ -13,7 +13,7 @@ import ( "html/template" ) -func rawHTML(content string) templ.Component { +func DocsLayout(nav Navigation, doc Document, user User) templ.Component { return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { @@ -34,50 +34,16 @@ func rawHTML(content string) templ.Component { templ_7745c5c3_Var1 = templ.NopComponent } ctx = templ.ClearChildren(ctx) - var templ_7745c5c3_Var2 string - templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(template.HTML(content)) - if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 9, Col: 25} - } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) - if templ_7745c5c3_Err != nil { - return templ_7745c5c3_Err - } - return nil - }) -} - -func DocsLayout(nav Navigation, doc Document, user User) templ.Component { - return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) { - templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context - if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil { - return templ_7745c5c3_CtxErr - } - templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W) - if !templ_7745c5c3_IsBuffer { - defer func() { - templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer) - if templ_7745c5c3_Err == nil { - templ_7745c5c3_Err = templ_7745c5c3_BufErr - } - }() - } - ctx = templ.InitializeContext(ctx) - templ_7745c5c3_Var3 := templ.GetChildren(ctx) - if templ_7745c5c3_Var3 == nil { - templ_7745c5c3_Var3 = templ.NopComponent - } - ctx = templ.ClearChildren(ctx) templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "") if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var4 string - templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) + var templ_7745c5c3_Var2 string + templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 18, Col: 21} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 14, Col: 21} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -90,12 +56,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var5 string - templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) + var templ_7745c5c3_Var3 string + templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(section.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 191, Col: 22} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 187, Col: 22} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -113,12 +79,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var6 templ.SafeURL - templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) + var templ_7745c5c3_Var4 templ.SafeURL + templ_7745c5c3_Var4, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 196, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 192, Col: 27} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var4)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -126,12 +92,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var7 string - templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) + var templ_7745c5c3_Var5 string + templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 197, Col: 21} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 193, Col: 21} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -139,12 +105,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var8 string - templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) + var templ_7745c5c3_Var6 string + templ_7745c5c3_Var6, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 197, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 193, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var6)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -167,12 +133,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var9 templ.SafeURL - templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) + var templ_7745c5c3_Var7 templ.SafeURL + templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinURLErrs(item.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 204, Col: 27} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 200, Col: 27} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -180,12 +146,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var10 string - templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) + var templ_7745c5c3_Var8 string + templ_7745c5c3_Var8, templ_7745c5c3_Err = templ.JoinStringErrs(item.Icon) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 205, Col: 21} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 201, Col: 21} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var8)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -193,12 +159,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var11 string - templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) + var templ_7745c5c3_Var9 string + templ_7745c5c3_Var9, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 205, Col: 36} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 201, Col: 36} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var9)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -237,12 +203,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var12 templ.SafeURL - templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinURLErrs(crumb.URL) + var templ_7745c5c3_Var10 templ.SafeURL + templ_7745c5c3_Var10, templ_7745c5c3_Err = templ.JoinURLErrs(crumb.URL) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 223, Col: 26} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 219, Col: 26} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var10)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -250,12 +216,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var13 string - templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Title) + var templ_7745c5c3_Var11 string + templ_7745c5c3_Var11, templ_7745c5c3_Err = templ.JoinStringErrs(crumb.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 223, Col: 42} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 219, Col: 42} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var11)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -273,12 +239,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var14 string - templ_7745c5c3_Var14, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) + var templ_7745c5c3_Var12 string + templ_7745c5c3_Var12, templ_7745c5c3_Err = templ.JoinStringErrs(doc.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 229, Col: 48} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 225, Col: 48} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var12)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -296,12 +262,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var15 templ.SafeURL - templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor) + var templ_7745c5c3_Var13 templ.SafeURL + templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinURLErrs("#" + item.Anchor) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 236, Col: 34} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 232, Col: 34} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -309,12 +275,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var16 string - templ_7745c5c3_Var16, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level)) + var templ_7745c5c3_Var14 string + templ_7745c5c3_Var14, templ_7745c5c3_Err = templruntime.SanitizeStyleAttributeValues("margin-left: " + fmt.Sprintf("%drem", item.Level)) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 236, Col: 112} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 232, Col: 112} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var14)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -322,12 +288,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - var templ_7745c5c3_Var17 string - templ_7745c5c3_Var17, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) + var templ_7745c5c3_Var15 string + templ_7745c5c3_Var15, templ_7745c5c3_Err = templ.JoinStringErrs(item.Title) if templ_7745c5c3_Err != nil { - return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 237, Col: 20} + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 233, Col: 20} } - _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var17)) + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var15)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } @@ -345,7 +311,12 @@ func DocsLayout(nav Navigation, doc Document, user User) templ.Component { if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } - templ_7745c5c3_Err = rawHTML(doc.Content).Render(ctx, templ_7745c5c3_Buffer) + var templ_7745c5c3_Var16 string + templ_7745c5c3_Var16, templ_7745c5c3_Err = templ.JoinStringErrs(template.HTML(doc.Content)) + if templ_7745c5c3_Err != nil { + return templ.Error{Err: templ_7745c5c3_Err, FileName: `docs.templ`, Line: 241, Col: 33} + } + _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var16)) if templ_7745c5c3_Err != nil { return templ_7745c5c3_Err } diff --git a/templates/types.go b/templates/types.go index b9adc6a..b8e3aed 100644 --- a/templates/types.go +++ b/templates/types.go @@ -160,3 +160,6 @@ type BreadcrumbItem struct { Title string URL string } + +// UnsafeHTML is used for rendering raw HTML (like rendered markdown) +type UnsafeHTML string