22 KiB
Architecture Overview
PersoNotes is a web-based Markdown note-taking application built with a hybrid architecture combining Go backend, HTMX for interactions, and modern JavaScript for UI enhancements.
Design Philosophy
HTML Over The Wire: The server renders HTML, not JSON. HTMX enables dynamic interactions without building a full SPA.
Progressive Enhancement: Core functionality works with basic HTTP. JavaScript enhances the experience (CodeMirror editor, drag-and-drop, search modal).
Simplicity First: Avoid framework complexity. Use the right tool for each job:
- Go for backend (fast, simple, type-safe)
- HTMX for AJAX (declarative, low JavaScript)
- Vanilla JS for UI (no framework overhead)
- Vite for building (fast, modern)
System Architecture
┌─────────────────────────────────────────────────────────────┐
│ Browser (Client) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ HTMX │ │ CodeMirror │ │ JavaScript │ │
│ │ (interactions)│ │ (editor) │ │ (UI logic) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ └──────────────────┴──────────────────┘ │
│ │ │
└────────────────────────────┼──────────────────────────────────┘
│ HTTP (HTML)
▼
┌─────────────────────────────────────────────────────────────┐
│ Go HTTP Server │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Handlers │ │ Indexer │ │ Watcher │ │
│ │ (API) │◄─┤ (search) │◄─┤ (fsnotify) │ │
│ └──────┬───────┘ └──────────────┘ └──────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Templates │ │
│ │ (Go html) │ │
│ └──────────────┘ │
│ │
└────────────────────────────┬────────────────────────────────┘
│ Filesystem
▼
┌─────────────────────────────────────────────────────────────┐
│ Markdown Files (.md) │
│ YAML Front Matter │
└─────────────────────────────────────────────────────────────┘
Component Interaction Patterns
1. Page Load (Initial Render)
User → Browser
│
├─ GET / → Go Server
│ │
│ ├─ Parse index.html template
│ ├─ Inject file tree
│ └─ Return HTML
│
├─ Load static/dist/personotes-frontend.es.js (Vite bundle)
│ │
│ ├─ Initialize FileTree (file-tree.js)
│ ├─ Initialize Search (search.js)
│ └─ Setup UI handlers (ui.js)
│
└─ HTMX processes hx-* attributes
│
└─ Triggers hx-get="/api/tree" (load file tree)
hx-get="/api/home" (load home page)
2. Opening a Note
User clicks file in tree → HTMX intercepts click (hx-get attribute)
│
├─ GET /api/notes/my-note.md → Go Server
│ │
│ ├─ Read file
│ ├─ Parse front matter
│ ├─ Render editor.html template
│ └─ Return HTML fragment
│
└─ HTMX swaps into #editor-container
│
└─ Triggers htmx:afterSwap event
│
└─ editor.js initializes CodeMirror
3. Drag and Drop File
User drags file → JavaScript (file-tree.js)
│
├─ dragstart: Store source path
├─ dragover: Validate drop target
└─ drop: Calculate destination
│
└─ htmx.ajax('POST', '/api/files/move')
│
├─ Go Server moves file
├─ Re-indexes
├─ Renders new file tree
└─ Returns HTML with hx-swap-oob="innerHTML" #file-tree
│
└─ HTMX swaps file tree automatically
│
└─ Triggers htmx:oobAfterSwap event
│
└─ file-tree.js updates draggable attributes
4. Searching Notes
User types in search → HTMX (hx-get="/api/search" with debounce)
│
├─ Go Server
│ │
│ ├─ Query indexer
│ ├─ Rank results
│ ├─ Render search-results.html
│ └─ Return HTML
│
└─ HTMX swaps into #search-results
Alternative: Search Modal (Ctrl/Cmd+K)
User presses Ctrl+K → search.js opens modal
│
└─ User types → Debounced fetch to /api/search
│
├─ Renders results in modal
└─ Keyboard navigation (JS)
5. Auto-Save in Editor
User types in editor → CodeMirror EditorView.updateListener
│
├─ Debounce 150ms → Update preview (JavaScript)
│
└─ Debounce 2s → Trigger save
│
├─ Sync content to hidden textarea
└─ form.requestSubmit()
│
└─ HTMX intercepts (hx-post="/api/notes/...")
│
├─ Go Server saves file
├─ Updates front matter (last_modified)
├─ Re-indexes
└─ Returns HTML with oob swap for file tree
│
└─ HTMX updates file tree automatically
6. Creating Links Between Notes (Internal Links)
User types /ilink → SlashCommands detects slash + query
│
├─ Filters commands by query
└─ Shows command palette
│
└─ User selects "ilink" → SlashCommands.openLinkInserter()
│
├─ Saves current cursor position
└─ Opens LinkInserter modal
│
User types query → LinkInserter searches
│
├─ Debounce 200ms
└─ fetch('/api/search?query=...')
│
├─ Go Server queries indexer
├─ Returns HTML results
└─ LinkInserter parses HTML
│
├─ Extracts title, path, tags
├─ Renders in modal
└─ Updates keyboard selection
│
User selects note → LinkInserter.selectResult()
(Enter/click) │
├─ Calls callback with {title, path}
└─ SlashCommands.openLinkInserter callback
│
├─ Builds HTML with HTMX: <a href="#" hx-get="/api/notes/path">title</a>
├─ Uses CodeMirror transaction
├─ Replaces /ilink with HTML link
├─ Positions cursor after link
└─ Closes modal
│
Preview Rendering → marked.js parses Markdown (including inline HTML)
│
├─ DOMPurify sanitizes (allows hx-* attributes)
├─ htmx.process() activates HTMX on links
└─ Links become clickable → load note via HTMX
Key Design Decisions:
- No new backend code: Reuses existing
/api/searchendpoint for search,/api/notes/for navigation - Database-free: Leverages in-memory indexer for speed
- Consistent UX: Modal design matches SearchModal styling
- Clickable links: HTML with HTMX attributes, rendered directly by marked.js
- HTMX integration: Links use
hx-getto load notes without page reload - Keyboard-first: Full keyboard navigation without mouse
Frontend Architecture
Build Process (Vite)
frontend/src/
├── main.js → Entry point
├── editor.js → CodeMirror 6 + Slash Commands
├── file-tree.js → Drag & drop + HTMX coordination
├── search.js → Search modal (Ctrl/Cmd+K)
└── ui.js → Sidebar toggle
↓ (Vite build)
static/dist/
├── personotes-frontend.es.js (1.0 MB - ES modules)
└── personotes-frontend.umd.js (679 KB - UMD)
↓ (Loaded by browser)
Executed in browser → Initializes components
Module Responsibilities
main.js
- Entry point
- Imports all modules
- No logic, just imports
editor.js
- MarkdownEditor class (CodeMirror 6)
- SlashCommands class (command palette)
- View mode management (split/editor-only/preview-only)
- Preview rendering (marked.js + DOMPurify)
- Scroll synchronization
- Auto-save logic
- HTMX event listeners for editor initialization
file-tree.js
- FileTree class (drag & drop)
- Event delegation for clicks (folder expand/collapse)
- Drag & drop event handlers
- htmx.ajax() for move operations
- Folder creation modal
- HTMX event listeners (htmx:oobAfterSwap) for updates
search.js
- Search modal (Ctrl/Cmd+K)
- Keyboard navigation
- Debounced search
- Result highlighting
- Uses HTMX search API
link-inserter.js
- LinkInserter class for internal note linking
- Modal search interface for
/ilinkcommand - Fuzzy search across notes
- Keyboard navigation (↑/↓/Enter/Esc)
- Integration with SlashCommands
- Uses HTMX search API for consistency
- Inserts Markdown links into editor
Note: /link command inserts standard Markdown template [texte](url) for external links
ui.js
- Sidebar toggle (mobile/desktop)
- Simple utility functions
HTMX Integration Patterns
Pattern 1: Declarative Links (Preferred)
Use HTMX attributes directly in HTML for static interactions:
<a href="#"
class="file-item"
hx-get="/api/notes/my-note.md"
hx-target="#editor-container"
hx-swap="innerHTML">
📄 my-note.md
</a>
When to use: Static content, links, forms with fixed targets.
Pattern 2: JavaScript-Initiated Requests
Use htmx.ajax() for dynamic interactions initiated by JavaScript:
htmx.ajax('POST', '/api/files/move', {
values: { source: 'old/path.md', destination: 'new/path.md' },
swap: 'none' // Server uses hx-swap-oob
});
When to use: Drag & drop, programmatic actions, complex validations.
Pattern 3: Out-of-Band Swaps (OOB)
Server includes additional HTML fragments to update multiple parts of the page:
<!-- Primary response -->
<div id="editor-container">
<!-- Editor HTML -->
</div>
<!-- Out-of-band swap (updates sidebar) -->
<div id="file-tree" hx-swap-oob="innerHTML">
<!-- Updated file tree -->
</div>
When to use: Updates to multiple unrelated parts of UI (e.g., save updates both editor status and file tree).
Pattern 4: Event Coordination
JavaScript listens to HTMX events to enhance behavior:
document.body.addEventListener('htmx:afterSwap', (event) => {
if (event.detail.target.id === 'editor-container') {
// Initialize CodeMirror after editor is loaded
initializeMarkdownEditor(event.detail.target);
}
});
document.body.addEventListener('htmx:oobAfterSwap', (event) => {
if (event.detail.target.id === 'file-tree') {
// Update draggable attributes after file tree updates
fileTree.updateDraggableAttributes();
}
});
When to use: Initialization, cleanup, progressive enhancement after HTML updates.
Backend Architecture
Request Flow
HTTP Request
│
▼
┌────────────────┐
│ Router │ Match route pattern
│ (ServeMux) │
└────┬───────────┘
│
▼
┌────────────────┐
│ Handler │ Parse request, validate input
│ (api package) │
└────┬───────────┘
│
├─ Read/Write Filesystem
│ (notes/*.md)
│
├─ Query Indexer
│ (search, tags)
│
└─ Render Template
(templates/*.html)
│
▼
HTML Response
Key Components
Indexer (internal/indexer/indexer.go)
- In-memory index:
map[string][]string(tag → files) - Document cache:
map[string]*Document(path → metadata) - Thread-safe with
sync.RWMutex - Parses YAML front matter
- Provides rich search (keywords, tags, title, path)
Watcher (internal/watcher/watcher.go)
- Uses
fsnotifyto monitor filesystem - Debounces events (200ms) to avoid re-index storms
- Recursively watches subdirectories
- Triggers indexer re-index on changes
API Handlers (internal/api/handler.go)
- Template rendering (Go
html/template) - CRUD operations (create, read, update, delete)
- Front matter management (auto-update last_modified)
- Path validation (prevent directory traversal)
- HTMX-friendly responses (HTML fragments + oob swaps)
Performance Optimizations
Frontend
-
Event Delegation: Attach listeners to parent elements, not individual items
- File tree clicks → Listen on
#sidebar, not each.file-item - Drag & drop → Listen on
#sidebar, not each draggable
- File tree clicks → Listen on
-
Debouncing:
- Editor preview update: 150ms
- Auto-save: 2 seconds
- Search: 500ms (declarative in HTMX)
-
HTMX Events over MutationObserver:
- Old: MutationObserver watching DOM continuously
- New: Listen to
htmx:afterSwapandhtmx:oobAfterSwap - Result: ~30% reduction in CPU usage during updates
-
Vite Code Splitting: Single bundle with all dependencies (avoids HTTP/2 overhead for small app)
Backend
- In-Memory Index: O(1) tag lookups, O(n) rich search
- Debounced Watcher: Prevent re-index storms during rapid file changes
- Graceful Shutdown: 5-second timeout for in-flight requests
- Template Caching: Pre-parse templates at startup (no runtime parsing)
Security
Frontend
- DOMPurify: Sanitizes Markdown-rendered HTML (prevents XSS)
- Path Validation: Client-side checks before sending to server
- No
eval(): No dynamic code execution - CSP-Ready: No inline scripts (all JS in external files)
Backend
-
Path Validation:
filepath.Clean()normalization- Reject
..(directory traversal) - Reject absolute paths
- Enforce
.mdextension - Use
filepath.Join()for safe concatenation
-
YAML Parsing: Uses
gopkg.in/yaml.v3(safe parser) -
No Code Execution: Server never executes user content
-
Graceful Error Handling: Errors logged, never exposed to client
API Security
Current State: No authentication Recommendation: Use reverse proxy (nginx/Caddy) with HTTP Basic Auth or OAuth2
location / {
auth_basic "PersoNotes";
auth_basic_user_file /etc/nginx/.htpasswd;
proxy_pass http://localhost:8080;
}
Testing Strategy
Frontend Testing
Manual Testing:
- File operations (open, edit, save, delete)
- Drag & drop (files, folders, edge cases)
- Search (keywords, tags, paths, quotes)
- Editor features (slash commands, preview, auto-save)
- Responsive design (mobile, tablet, desktop)
Browser Compatibility: Chrome, Firefox, Safari, Edge (modern evergreen browsers)
Backend Testing
Unit Tests: go test ./...
- Indexer: Front matter parsing, search ranking
- Path validation: Security checks
- Template rendering: Output validation
Integration Tests:
- File operations with real filesystem
- Watcher debouncing
- Concurrent access (race condition testing)
Run tests:
go test -v ./...
go test -race ./... # Detect race conditions
Deployment
Production Build
# 1. Build frontend
cd frontend
npm install
npm run build
cd ..
# 2. Build Go binary
go build -o server ./cmd/server
# 3. Run
./server -addr :8080 -notes-dir /path/to/notes
Docker Deployment
FROM golang:1.22-alpine AS builder
WORKDIR /app
# Build frontend
COPY frontend/package*.json frontend/
RUN cd frontend && npm install
COPY frontend/ frontend/
RUN cd frontend && npm run build
# Build Go binary
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN go build -o server ./cmd/server
# Runtime image
FROM alpine:latest
RUN apk add --no-cache ca-certificates
WORKDIR /app
COPY --from=builder /app/server .
COPY --from=builder /app/static ./static
COPY --from=builder /app/templates ./templates
VOLUME /app/notes
EXPOSE 8080
CMD ["./server", "-addr", ":8080", "-notes-dir", "/app/notes"]
Environment Variables
Not currently used - configuration via CLI flags only.
Future: Consider environment variables for production:
export NOTES_DIR=/data/notes
export SERVER_ADDR=:8080
export ENABLE_CORS=true
Monitoring and Observability
Logging
Current: log.Printf() to stdout
Recommended additions:
- Structured logging (JSON format)
- Log levels (DEBUG, INFO, WARN, ERROR)
- Request IDs for tracing
Metrics
Not currently implemented.
Recommended:
- Request count by endpoint
- Response time percentiles (p50, p95, p99)
- Indexer cache hit rate
- File operation errors
Tools: Prometheus + Grafana
Future Enhancements
Backend
- Full-text search with ranking (current: substring match)
- Note versioning (git integration?)
- Export notes (PDF, HTML, EPUB)
- Collaborative editing (WebSocket)
- Image upload and storage
Frontend
- Offline support (Service Worker)
- Mobile app (Capacitor wrapper)
- Keyboard shortcuts modal (show available shortcuts)
- Customizable editor themes
- Vim/Emacs keybindings
DevOps
- CI/CD pipeline (GitHub Actions)
- Automated backups
- Multi-user support (auth + permissions)
- Rate limiting
- CORS configuration
Contributing Guidelines
- Frontend changes: Build before testing (
npm run build) - Backend changes: Run tests (
go test ./...) - Architecture changes: Update this document
- New features: Add to CLAUDE.md for AI context
References
Last Updated: 2025-01-11 Architecture Version: 2.0 (Post-HTMX optimization)