Files
bookhoard/DOCS_IMPLEMENTATION_PLAN.md
T
john-okeefe 1e8cfa1008 Phase 1: Fix markdown rendering
- Add rawHTML helper function using template.HTML()
- Update docs template to use { template.HTML(doc.Content) }
- Docs now render HTML headings and content properly
- Markdown is converted to HTML by goldmark (with Unsafe()) and output directly
2026-02-01 21:05:12 -05:00

1310 lines
41 KiB
Markdown

# 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
<!-- In content section -->
<div>
@rawHTML(doc.Content)
</div>
```
#### 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
<!-- API Explorer will be inserted here in Phase 3 -->
```
#### 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
<div class="api-explorer">
<div class="api-mode-toggle">
<button class="mode-btn active">Mock Data</button>
<button class="mode-btn" disabled>Login to Try Real</button>
</div>
<div class="api-request-editor">
<textarea id="request-body" readonly>{ explorer.Endpoint.RequestBody }</textarea>
</div>
<div class="api-response">
<h4>Response (Mock)</h4>
<pre><code>{ template.HTML(explorer.Endpoint.Response) }</code></pre>
</div>
<button onclick="copyToClipboard(document.getElementById('request-body'))">Copy Request</button>
<button onclick="copyToClipboard(document.querySelector('.api-response pre'))">Copy Response</button>
</div>
} else {
// Show interactive explorer with real execution
<div class="api-explorer">
<div class="api-mode-toggle">
<button class="mode-btn" id="mock-btn">Mock Data</button>
<button class="mode-btn active" id="real-btn">Try It Out</button>
</div>
<div class="api-request-editor">
<div class="method-selector">
<select id="http-method">
<option value="GET" { if explorer.Endpoint.Method == "GET" } selected { end }>GET</option>
<option value="POST" { if explorer.Endpoint.Method == "POST" } selected { end }>POST</option>
<option value="PUT" { if explorer.Endpoint.Method == "PUT" } selected { end }>PUT</option>
<option value="DELETE" { if explorer.Endpoint.Method == "DELETE" } selected { end }>DELETE</option>
</select>
</div>
<textarea id="request-body" placeholder="Edit request body...">{ explorer.Endpoint.RequestBody }</textarea>
</div>
<button id="try-it-out">Try It Out</button>
<div class="api-response" style="display:none">
<div class="response-header">
<span id="response-status"></span>
<span id="response-time"></span>
</div>
<pre><code id="response-body"></code></pre>
</div>
<button onclick="copyToClipboard(document.getElementById('request-body'))">Copy Request</button>
<button onclick="copyToClipboard(document.getElementById('response-body'))">Copy Response</button>
<button onclick="generateCURL()">Generate cURL</button>
</div>
}
<script>
// Mode toggle logic
document.getElementById('mock-btn')?.addEventListener('click', () => showMode('mock'));
document.getElementById('real-btn')?.addEventListener('click', () => showMode('real'));
function showMode(mode) {
if (mode === 'mock') {
document.querySelector('.api-response').style.display = 'block';
document.getElementById('request-body').readOnly = true;
document.getElementById('try-it-out').style.display = 'none';
} else {
document.querySelector('.api-response').style.display = 'none';
document.getElementById('request-body').readOnly = false;
document.getElementById('try-it-out').style.display = 'block';
}
}
// Try it out logic
document.getElementById('try-it-out')?.addEventListener('click', async () => {
const method = document.getElementById('http-method').value;
const body = document.getElementById('request-body').value;
const endpoint = '{ explorer.Endpoint.Path }';
const startTime = Date.now();
try {
const response = await fetch(endpoint, {
method: method,
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + localStorage.getItem('token')
},
body: body ? body : undefined
});
const duration = Date.now() - startTime;
const data = await response.json();
document.getElementById('response-status').textContent = `${response.status} (${response.statusText})`;
document.getElementById('response-time').textContent = `${duration}ms`;
document.getElementById('response-body').textContent = JSON.stringify(data, null, 2);
document.querySelector('.api-response').style.display = 'block';
} catch (error) {
document.getElementById('response-status').textContent = 'Error';
document.getElementById('response-body').textContent = error.message;
document.querySelector('.api-response').style.display = 'block';
}
});
function copyToClipboard(text) {
navigator.clipboard.writeText(text);
}
function generateCURL() {
const method = document.getElementById('http-method').value;
const body = document.getElementById('request-body').value;
const endpoint = '{ explorer.Endpoint.Path }';
const token = localStorage.getItem('token');
const curl = `curl -X ${method} \\\n -H "Content-Type: application/json" \\\n -H "Authorization: Bearer ${token}" \\\n -d '${body}' \\\n ${endpoint}`;
copyToClipboard(curl);
}
</script>
<style>
.api-explorer {
border: 1px solid var(--border);
border-radius: 8px;
padding: 1.5rem;
margin-top: 2rem;
background: var(--bg-secondary);
}
.api-mode-toggle {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.mode-btn {
padding: 0.5rem 1rem;
border: 1px solid var(--border);
border-radius: 4px;
background: var(--bg-primary);
color: var(--text-primary);
cursor: pointer;
}
.mode-btn.active {
background: var(--accent);
color: white;
}
.mode-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.api-request-editor {
margin-bottom: 1rem;
}
textarea#request-body {
width: 100%;
min-height: 150px;
padding: 0.75rem;
font-family: 'Monaco', 'Consolas', monospace;
font-size: 0.875rem;
background: var(--bg-primary);
color: var(--text-primary);
border: 1px solid var(--border);
border-radius: 4px;
}
.api-response {
margin-top: 1rem;
}
.response-header {
display: flex;
justify-content: space-between;
margin-bottom: 0.5rem;
font-size: 0.875rem;
}
#response-status {
font-weight: 600;
}
#response-time {
color: var(--text-secondary);
}
.api-response pre {
background: var(--bg-primary);
padding: 1rem;
border-radius: 4px;
overflow-x: auto;
}
#try-it-out {
background: var(--accent);
color: white;
padding: 0.75rem 1.5rem;
border: none;
border-radius: 4px;
cursor: pointer;
margin-top: 1rem;
}
#try-it-out:hover {
opacity: 0.9;
}
</style>
}
```
#### 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) {
<!DOCTYPE html>
<!-- ... existing HTML ... -->
if explorer != nil {
@APIExplorer(*explorer)
}
<!-- ... rest of template ... -->
}
```
---
## 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 `<head>`**:
```html
<script src="https://cdn.jsdelivr.net/npm/lunr@2.3.9/lunr.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lunr-flex@1.0.5/lunr.flex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lunr-highlight@1.0.0/lunr.highlight.min.js"></script>
```
**Add search input in sidebar**:
```html
<!-- In sidebar -->
<div class="search-box">
<input type="text"
id="search-input"
placeholder="Search documentation..."
autocomplete="off">
</div>
```
**Add search results overlay**:
```html
<!-- After main content -->
<div id="search-results" style="display:none"></div>
```
**Add search JavaScript**:
```html
<script>
// Search state
let idx;
let searchResults = [];
// Load index on page load
fetch('/docs/search-index.json')
.then(r => r.json())
.then(data => {
// Initialize Lunr with fuzzy matching
idx = lunr(function() {
this.use(lunr.flex) // Enable fuzzy search
this.ref('id')
this.field('title', {boost: 10}) // Boost title matches
this.field('content', {boost: 1})
data.forEach(doc => this.add(doc))
})
console.log('Search index loaded:', data.length, 'documents')
})
.catch(err => console.error('Failed to load search index:', err));
// Search input with debounce
const searchInput = document.getElementById('search-input');
const searchResultsDiv = document.getElementById('search-results');
let debounceTimer;
searchInput.addEventListener('input', (e) => {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
const query = e.target.value.trim();
if (query.length < 2) {
searchResultsDiv.style.display = 'none';
return;
}
// Search with fuzzy matching
const results = idx.search(query, {
fields: {title: {boost: 10}, content: 1},
expand: true // Enable fuzzy matching
});
// Store results for highlighting
searchResults = results;
// Render results
renderSearchResults(results, query);
}, 150); // 150ms debounce
});
// Render search results
function renderSearchResults(results, query) {
if (results.length === 0) {
searchResultsDiv.innerHTML = '<div class="no-results">No results found</div>';
searchResultsDiv.style.display = 'block';
return;
}
searchResultsDiv.innerHTML = results.map(r => {
// Highlight matched terms
const highlightedTitle = lunr.highlight(r.matchData.metadata.title, r.queryTerms, {
pre: '<mark>',
post: '</mark>'
});
const highlightedContent = lunr.highlight(r.matchData.metadata.content, r.queryTerms, {
pre: '<mark>',
post: '</mark>',
length: 150 // Limit snippet length
});
return `
<a href="${r.ref}" class="search-result">
<div class="result-title">${highlightedTitle}</div>
<div class="result-snippet">${highlightedContent}...</div>
</a>
`;
}).join('');
searchResultsDiv.style.display = 'block';
}
// Close search results when clicking outside
document.addEventListener('click', (e) => {
if (!searchResultsDiv.contains(e.target) && e.target !== searchInput) {
searchResultsDiv.style.display = 'none';
}
});
</script>
```
**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
<header class="docs-header">
<button id="sidebar-toggle" class="mobile-menu-btn" style="display:none;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
</svg>
</button>
</header>
```
**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
<script>
const sidebarToggle = document.getElementById('sidebar-toggle');
const sidebar = document.getElementById('sidebar');
sidebarToggle.addEventListener('click', () => {
sidebar.classList.toggle('open');
});
// Close sidebar when clicking main content
document.querySelector('.main-content').addEventListener('click', () => {
sidebar.classList.remove('open');
});
</script>
```
#### 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