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