- 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
1310 lines
41 KiB
Markdown
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
|