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

41 KiB

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 rawHTML(content string) {
    { template.HTML(content) }
}

Solution: Use @rawHTML directive instead of function call

<!-- In content section -->
<div>
    @rawHTML(doc.Content)
</div>

1.2 Test docs rendering

Action:

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:

# 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:

# 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)

{
  "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
        <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:

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:

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 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:

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:

// 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:

// 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>:

<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:

<!-- In sidebar -->
<div class="search-box">
    <input type="text"
           id="search-input"
           placeholder="Search documentation..."
           autocomplete="off">
</div>

Add search results overlay:

<!-- After main content -->
<div id="search-results" style="display:none"></div>

Add search JavaScript:

<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:

/* 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:

<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:

.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:

<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:

@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

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)

  1. Phase 4: Lunr.js Search (2-3 hours)
  2. Phase 2: Split API Docs (2-3 hours)
  3. Sprint 2 test: Verify all features work

Sprint 3: Advanced Features (4-6 hours)

  1. Phase 3: Interactive API Explorer (3-4 hours)
  2. Phase 5: Mobile Responsive Design (1-2 hours)

Sprint 4: Polish (1-2 hours)

  1. 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


Last Updated: 2026-02-01 Next Review: After Sprint 1 completion