diff --git a/COLLECTIONS_CLEANUP_GUIDE.md b/COLLECTIONS_CLEANUP_GUIDE.md
index afe8dc2..41a71dc 100644
--- a/COLLECTIONS_CLEANUP_GUIDE.md
+++ b/COLLECTIONS_CLEANUP_GUIDE.md
@@ -1,8 +1,12 @@
-# Alpine.js and SSR Architecture Cleanup Guide
+# Collections and Alpine.js Cleanup Guide
## Overview
-This guide provides a systematic, page-by-page approach to fixing Alpine.js issues while maintaining SSR-first architecture.
+This guide provides fixes for Alpine.js issues while maintaining SSR-first architecture.
+
+**References:**
+- **`SSR_FIRST_ALPINE_GUIDE.md`** - Complete SSR-first architecture principles (READ THIS FIRST)
+- **`ALPINE_COMPLETION_GUIDE.md`** - Full reactive Alpine.js migration (eventual goal)
### Problem Statement
@@ -12,43 +16,41 @@ Console errors caused by:
3. Missing x-init calls in templates
4. **CRITICAL:** x-init functions that fetch data and replace SSR content
-### SSR-First Alpine.js Strategy
+### SSR-First Principles
-**Different pages have different SSR/JavaScript ratios:**
+**See `SSR_FIRST_ALPINE_GUIDE.md` for complete documentation**
-- **Analytics Page (80% JavaScript):** Dynamic charts/data that fetch on page load
- - ✅ `x-init="loadAnalytics"` fetches data and replaces content
- - ✅ This is intentional - analytics is a JavaScript-heavy page
+**Quick Summary:**
+- ❌ **NEVER fetch data in x-init** if data is already SSR'd
+- ✅ x-init ONLY for setup (event listeners, modals)
+- ✅ Data fetch ONLY after user actions (create/delete/update)
+- ✅ State lives in template (`x-data`), not in TypeScript
-- **Dashboard Page (SSR + Interactive):** SSR for speed, JS for drag-drop/library switching
- - ✅ SSR provides initial dashboard data
- - ✅ x-init ONLY sets up event listeners (drag-drop, modals)
- - ✅ x-init does NOT fetch data (data already SSR'd)
- - ✅ `switchLibrary()` fetches only when user changes library dropdown
+### Page Types
-- **Admin Library Page (SSR + Modals/CRUD):** SSR list, JS for management
- - ✅ SSR provides initial library list
- - ✅ x-init ONLY sets up event listeners and modals
- - ❌ BUG: `initializeLibraryAdmin()` was calling `reloadLibraries()` which fetches data
- - ❌ This replaces the SSR content on page load - MUST FIX
+**Type 1: 80% SSR** (Collections, Conflicts, Queue)
+- Backend provides all data
+- Alpine for modals only
+- No data fetch in x-init
-- **Most Pages (80% SSR):** Server-rendered with interactive areas
- - ✅ Use x-init for setup only (event listeners, modals)
- - ❌ NEVER fetch data in x-init if data is already SSR'd
+**Type 2: SSR + Interactive** (Dashboard, Admin Library)
+- Backend provides initial data
+- Alpine for interactivity (drag-drop, CRUD)
+- x-init sets up listeners only
-**Key Principle:**
-- ✅ DO in x-init: Set up event listeners, initialize modals, setup drag-drop
-- ❌ DON'T in x-init: Fetch data via API, replace SSR innerHTML
+**Type 3: 80% JavaScript** (Analytics)
+- Backend renders empty shell
+- x-init fetches ALL data (intentional)
### Review Status
**Pages methodically reviewed so far:**
-- ✅ **Dashboard** - Completed, working correctly
-- 🔄 **Collections** - In progress (fixes in progress)
-- ⏸️ **Admin Library** - Identified bug, fix documented in this guide
-- ⏸️ **Other pages** - Not yet reviewed, will be done page-by-page
+- ✅ **Dashboard** - Completed
+- 🔄 **Collections** - In progress (this guide)
+- ⏸️ **Admin Library** - Fixed (see SSR_FIRST_ALPINE_GUIDE.md)
+- ⏸️ **Other pages** - Not yet reviewed
-**Goal:** Clean, working Alpine.js integration with proper SSR architecture.
+**Goal:** Fix console errors and maintain SSR architecture.
---
@@ -322,7 +324,7 @@ Alpine.data("analytics", () => ({
```
-### Step 3.2: docs.ts - SIMPLE SETUP, NO DATA FETCH
+### Step 3.2: docs.ts - SIMPLE SETUP ONLY
**File:** `web/src/docs.ts`
@@ -333,20 +335,9 @@ Alpine.data("analytics", () => ({
document.addEventListener("DOMContentLoaded", () => {
initializeDocsSearch();
});
-
-export { toggleSidebar, initializeDocsSearch };
-
-Alpine.data("docs", () => ({
- toggleSidebar,
- initializeSearch: initializeDocsSearch,
-}));
```
-**TWO OPTIONS:**
-
----
-
-### **OPTION A: Use x-init (Simple)**
+**Solution:** Remove DOMContentLoaded, use x-init
**Remove DOMContentLoaded:**
```typescript
@@ -369,54 +360,21 @@ Alpine.data("docs", () => ({
```
-**✅ Simple**
-**✅ Clear initialization**
+**✅ Simple setup only**
+**✅ No data fetch** (search is client-side)
+**✅ x-init is appropriate here**
----
+### Step 3.3: library.ts - ALREADY FIXED
-### **OPTION B: Event Delegation Pattern (See ALPINE_COMPLETION_GUIDE.md)**
+**Status:** ✅ **COMPLETED** - See commit 1b9bc64
-**If the search input uses data-action attributes, use global event delegation:**
+**The SSR bug has been fixed:**
+- Removed `void reloadLibraries()` from `initializeLibraryAdmin()`
+- SSR provides initial library list (no fetch on page load)
+- `reloadLibraries()` available for after CRUD operations
+- See `SSR_FIRST_ALPINE_GUIDE.md` for complete SSR-first principles
-**Remove DOMContentLoaded and initializeDocsSearch entirely:**
-```typescript
-// DELETE both:
-// document.addEventListener("DOMContentLoaded", () => {
-// initializeDocsSearch();
-// });
-// function initializeDocsSearch() { /* ... */ }
-
-// Export only business logic
-export { toggleSidebar }; // Keep if used
-
-// Search setup via global event delegation in main.ts or separate file
-```
-
-**Template:**
-```html
-
-
-
-```
-
-**✅ No initialization**
-**✅ Works with existing event delegation**
-
----
-
-### **Recommendation: OPTION A**
-
-Since `initializeDocsSearch()` doesn't fetch data and only sets up a listener, using x-init is fine and keeps the code clear.
-
-### Step 3.3: library.ts - TWO APPROACHES
-
-**⚠️ CRITICAL SSR BUG:** The `initializeLibraryAdmin()` function currently calls `reloadLibraries()` which fetches data from the API and replaces the SSR-rendered library list.
-
-**Reference:** The full Alpine.js reactive pattern is documented in `ALPINE_COMPLETION_GUIDE.md`
-
-**File:** `web/src/library.ts`
-
-**Current Problem (around line 653-655):**
+**Current state (web/src/library.ts:653-655):**
```typescript
function initializeLibraryAdmin(): void {
// Setup event listeners
@@ -425,210 +383,21 @@ function initializeLibraryAdmin(): void {
librariesList.addEventListener("click", handleLibraryListClick);
}
- // ... more setup ...
+ // ... setup code ...
- // ❌ BUG: This fetches data and replaces SSR content on page load!
- void reloadLibraries();
+ // ✅ FIXED: No data fetch - SSR provides initial library list
+ // reloadLibraries() is called AFTER create/delete/update operations only
}
```
-**TWO SOLUTIONS - Choose one:**
-
----
-
-### **OPTION A: Minimal Fix (Quick) - Remove Data Fetch from Init**
-
-**Fix library.ts:**
-```typescript
-function initializeLibraryAdmin(): void {
- // Setup event listeners
- const librariesList = document.getElementById("libraries-list");
- if (librariesList) {
- librariesList.addEventListener("click", handleLibraryListClick);
- }
-
- // ... keep all existing setup code ...
-
- // ✅ REMOVED: void reloadLibraries();
- // SSR provides initial library list - no need to fetch on page load
- // reloadLibraries() is still available to call AFTER create/delete/update operations
-}
-```
-
-**Template remains:**
+**Template (templates/admin_library.templ:11):**
```html
```
-**✅ Keeps current architecture**
-**✅ Minimal code changes**
-**⚠️ Still uses manual event listeners**
+**✅ No changes needed** - SSR bug is already fixed.
----
-
-### **OPTION B: Full Alpine.js Reactive Pattern (Recommended) - See ALPINE_COMPLETION_GUIDE.md**
-
-**This approach eliminates ALL manual DOM manipulation and event listeners.**
-
-**Step 1: Update template to use reactive state:**
-
-**File:** `templates/admin_library.templ`
-
-**Find modals (around lines 93-155):**
-
-**Current (create library modal):**
-```html
-
-
-
Create Library
-
-
-
-
-```
-
-**Change to:**
-```html
-
-
-
-
-
-
-
-
-
Create Library
-
-
-
-
-
-
-
-```
-
-**Update button to open modal (line 22):**
-```html
-
-
-
-
-
-```
-
-**Step 2: Remove manual DOM manipulation from library.ts:**
-
-**Delete these functions (no longer needed):**
-```typescript
-// DELETE ALL:
-function showCreateLibraryModal() { /* ... */ }
-function hideCreateLibraryModal() { /* ... */ }
-function showDeleteModal() { /* ... */ }
-function hideDeleteModal() { /* ... */ }
-function showFolderBrowser() { /* ... */ }
-function hideFolderBrowser() { /* ... */ }
-
-// DELETE initializeLibraryAdmin entirely:
-function initializeLibraryAdmin() { /* ... */ }
-```
-
-**Keep ONLY business logic functions:**
-```typescript
-// KEEP - These are pure business logic, no DOM manipulation:
-async function handleCreateLibrarySubmit(event: Event) { /* ... */ }
-async function deleteLibrary(libraryId: string) { /* ... */ }
-async function addLibraryFolder(libraryId: string) { /* ... */ }
-async function removeLibraryFolder(libraryId: string, folderPath: string) { /* ... */ }
-async function showLibraryFolders(libraryId: string) { /* renders folders via innerHTML - keep for now */ }
-async function loadUserVisibility() { /* renders checkboxes via innerHTML - keep for now */ }
-async function setLibraryVisibility(userId: string, libraryId: string, isVisible: boolean) { /* ... */ }
-```
-
-**Step 3: Remove x-init from template:**
-
-**File:** `templates/admin_library.templ` (line 11)
-
-```html
-
-
-
-
-
-```
-
-**Step 4: Update Alpine.data registration:**
-
-**File:** `web/src/library.ts` (bottom of file)
-
-```typescript
-// BEFORE:
-Alpine.data("library", () => ({
- addLibraryFolder,
- confirmDeleteLibrary,
- deleteLibrary,
- editLibrary,
- handleCreateLibrarySubmit,
- hideDeleteModal,
- hideFolderBrowser,
- initializeLibraryAdmin, // ← Remove this
- loadUserVisibility,
- navigateFolderBrowser,
- removeLibraryFolder,
- selectBrowseFolder,
- setLibraryVisibility,
- showDeleteModal,
- showFolderBrowser,
- showLibraryFolders,
-}));
-
-// AFTER:
-Alpine.data("library", () => ({
- // Only business logic functions
- addLibraryFolder,
- confirmDeleteLibrary,
- deleteLibrary,
- editLibrary,
- handleCreateLibrarySubmit,
- loadUserVisibility,
- navigateFolderBrowser,
- removeLibraryFolder,
- selectBrowseFolder,
- setLibraryVisibility,
- showLibraryFolders,
-}));
-```
-
-**✅ Eliminates ALL manual DOM manipulation**
-**✅ No initialization needed**
-**✅ Reactive state in template**
-**✅ Follows full Alpine.js pattern from ALPINE_COMPLETION_GUIDE.md**
-
----
-
-### **Which Option to Choose?**
-
-**Choose Option A (Minimal Fix) if:**
-- You want to fix the SSR bug quickly
-- You're comfortable with manual event listeners
-- You plan to refactor to full Alpine.js later
-
-**Choose Option B (Full Alpine.js) if:**
-- You want the cleanest architecture
-- You want to eliminate all manual DOM manipulation
-- You're following the pattern in ALPINE_COMPLETION_GUIDE.md
-- You want smooth transitions and better UX
-
-**Recommendation:** **Option B** - Full Alpine.js reactive pattern. It's cleaner, more maintainable, and aligns with the long-term architecture goal.
-
-### Step 3.4: dashboard.ts - EVENT DELEGATION ALREADY EXISTS
+### Step 3.4: dashboard.ts - WRAP EXISTING CODE
**File:** `web/src/dashboard.ts`
@@ -637,7 +406,22 @@ Alpine.data("library", () => ({
**Current (lines 494-521):**
```typescript
document.addEventListener("DOMContentLoaded", () => {
- initDragAndDrop(); // ← Drag-drop initialization
+ initDragAndDrop();
+
+ document.addEventListener("click", (e: Event) => {
+ // ... event delegation with data-action ...
+ });
+
+ // ... library select setup ...
+});
+```
+
+**Solution:** Wrap existing DOMContentLoaded code in `initDashboard()` function
+
+**Create wrapper function at end of dashboard.ts:**
+```typescript
+function initDashboard() {
+ initDragAndDrop(); // Setup drag-drop
document.addEventListener("click", (e: Event) => {
const target = e.target as HTMLElement;
@@ -646,34 +430,20 @@ document.addEventListener("DOMContentLoaded", () => {
switch (action) {
case "scroll-carousel": /* ... */ break;
- // ... more cases ...
+ // ... existing cases ...
}
});
- // ... library select setup ...
-});
-```
-
-**TWO OPTIONS:**
-
----
-
-### **OPTION A: Wrap in initDashboard() function**
-
-**Create wrapper function at end of dashboard.ts:**
-```typescript
-function initDashboard() {
- initDragAndDrop();
-
- document.addEventListener("click", (e: Event) => {
- // ... existing click handler code ...
- });
-
document.addEventListener("input", (e: Event) => {
- // ... existing input handler code ...
+ // ... existing input handler ...
});
- // ... library select and localStorage code ...
+ // Load saved library on page load
+ const savedLibrary = localStorage.getItem("selectedLibrary");
+ const currentLibrary = new URLSearchParams(window.location.search).get("library_id");
+ if (savedLibrary && savedLibrary !== currentLibrary) {
+ window.location.href = `/dashboard?library_id=${savedLibrary}`;
+ }
}
export { initDashboard };
@@ -689,64 +459,10 @@ Alpine.data("dashboard", () => ({
```
-**✅ Keeps current event delegation pattern**
-**✅ Simple wrapper function**
-**⚠️ Still runs on page load**
-
----
-
-### **OPTION B: Remove DOMContentLoaded, Rely on Global Event Delegation**
-
-**Since dashboard already uses data-action attributes with global listeners, we can remove DOMContentLoaded entirely:**
-
-**Remove from dashboard.ts (lines 494-521):**
-```typescript
-// DELETE this entire block:
-document.addEventListener("DOMContentLoaded", () => {
- // ... all of it ...
-});
-```
-
-**Move drag-drop initialization to separate function (can be called later if needed):**
-```typescript
-// Drag-drop requires setup after content loads
-// Since dashboard content is SSR'd, we can initialize immediately
-function initDragAndDrop() {
- // ... existing drag-drop code ...
-}
-
-// Call it directly (no need to wait for DOMContentLoaded)
-initDragAndDrop();
-
-// Global event listeners are already set up in main.ts or run immediately
-document.addEventListener("click", (e: Event) => {
- // ... existing click handler ...
-});
-
-document.addEventListener("input", (e: Event) => {
- // ... existing input handler ...
-});
-
-// ... library select setup ...
-```
-
-**No template changes needed:**
-```html
-
-
-
-```
-
-**✅ No initialization on page load**
-**✅ Relies on existing event delegation**
-**✅ SSR-first compatible**
-**⚠️ Drag-drop initializes immediately (might be too early)**
-
----
-
-### **Recommendation: OPTION A**
-
-Use the wrapper function approach. The dashboard has complex initialization (drag-drop, localStorage check) that benefits from running after Alpine loads. The event delegation pattern is already correct - just wrap it properly.
+**✅ Keeps event delegation pattern**
+**✅ Wraps existing code (minimal changes)**
+**✅ x-init does NOT fetch data (SSR provides initial dashboard)**
+**✅ localStorage redirect is user preference, not data fetch**
**Create a wrapper function at the end of the file:**
@@ -921,8 +637,8 @@ go build ./cmd/server
**Pages methodically reviewed so far:**
- ✅ **Dashboard** - Completed, working correctly with SSR
-- 🔄 **Collections** - In progress (fixes in progress)
-- ⏸️ **Admin Library** - SSR bug identified, TWO fix approaches documented
+- ✅ **Admin Library** - SSR bug fixed (commit 1b9bc64)
+- 🔄 **Collections** - In progress (this guide)
- ⏸️ **Other pages** - Not yet reviewed, will be done page-by-page
### Completed Changes
@@ -931,45 +647,27 @@ go build ./cmd/server
- `web/src/collections.ts` - ✅ Removed dead Alpine.js exports
- `web/src/analytics.ts` - ✅ Removed DOMContentLoaded, SSR-first (intentional data fetch)
- `web/src/admin.ts` - ✅ Removed DOMContentLoaded, WebSocket moved to template
+- `web/src/library.ts` - ✅ Fixed SSR bug, removed reloadLibraries() from init
**Template Files (1 file):**
- `templates/analytics.templ` - ✅ Added x-init="loadAnalytics" (JS-heavy page, correct)
-### Two Approaches Available
-
-**This guide now presents TWO approaches for each page:**
-
-**OPTION A: Minimal Fix (Quick)**
-- Fix SSR bugs by removing data fetch from init functions
-- Keep x-init for setup only
-- Keep current event listener patterns
-- Good for quick fixes
-
-**OPTION B: Full Alpine.js Reactive Pattern (Recommended - see ALPINE_COMPLETION_GUIDE.md)**
-- Eliminate ALL manual DOM manipulation
-- Use x-data for state, x-show for visibility
-- Use @click.outside for closing dropdowns/modals
-- Use x-transition for smooth animations
-- No initialization functions needed
-- Aligns with long-term architecture
-
-**Reference:** `ALPINE_COMPLETION_GUIDE.md` contains the complete Alpine.js pattern with examples
-
### Pending Fixes
-**Admin Library (Step 3.3) - TWO approaches:**
-- **Option A:** Remove `reloadLibraries()` from `initializeLibraryAdmin()` - Quick fix
-- **Option B:** Full Alpine.js reactive pattern - Eliminate all manual DOM manipulation
-- Template already has `x-data="library" x-init="initializeLibraryAdmin"`
+**Collections (This Guide):**
+- Dead exports removed (Step 1)
+- DOMContentLoaded cleanup (Step 3)
+- x-init calls needed (Step 3)
-**Dashboard (Step 3.4):**
-- **Option A:** Wrap existing code in `initDashboard()` function
-- **Option B:** Remove DOMContentLoaded, rely on event delegation
-- Already uses data-action event delegation correctly
+**Dashboard:**
+- Wrap existing code in `initDashboard()` function
+- Add x-init to template
+- Already uses event delegation correctly
-**Docs (Step 3.2):**
-- **Option A:** Use x-init for search setup (simple, no data fetch)
-- **Option B:** Use event delegation pattern
+**Docs:**
+- Remove DOMContentLoaded
+- Add x-init="initializeDocsSearch" to template
+- Simple setup only, no data fetch
### What Was NOT Changed
@@ -980,58 +678,32 @@ go build ./cmd/server
### SSR-First Principles
-✅ **Analytics (80% JS):** x-init fetches data - intentional for dynamic page
-✅ **Dashboard (SSR + JS):** x-init ONLY sets up event listeners, no data fetch
-✅ **Admin Library:** Choose Option A (quick) or Option B (full Alpine.js)
-✅ **Collections:** SSR provides data, JS for interactivity (in progress)
+**See `SSR_FIRST_ALPINE_GUIDE.md` for complete documentation:**
+
+✅ **Type 1 (80% SSR):** Collections, Conflicts - backend provides data, Alpine for modals only
+✅ **Type 2 (SSR + Interactive):** Dashboard, Admin Library - SSR data + Alpine for interactivity
+✅ **Type 3 (80% JS):** Analytics - x-init fetches data (intentional)
**Key Principle:**
-- ✅ DO in x-init: Set up event listeners, initialize modals, setup drag-drop
-- ❌ DON'T in x-init: Fetch data via API, replace SSR innerHTML
-
-### Architecture Decision Point
-
-**You're at a decision point:**
-
-**Path 1: Quick Fixes (Option A for all pages)**
-- Fix SSR bugs by removing data fetch from inits
-- Keep current event listener patterns
-- Faster to complete
-- Can refactor to full Alpine.js later
-
-**Path 2: Full Alpine.js Migration (Option B for all pages)**
-- Follow ALPINE_COMPLETION_GUIDE.md pattern
-- Eliminate all manual DOM manipulation
-- Cleaner long-term architecture
-- More work upfront, but better result
-
-**Recommendation:** Mix both approaches based on page complexity:
-- Simple pages (docs): Use Option A
-- Complex pages (admin library): Use Option B (full Alpine.js)
-- This lets you learn the pattern progressively
+- ❌ **NEVER fetch data in x-init** if data is already SSR'd
+- ✅ x-init ONLY for setup (event listeners, modals)
+- ✅ Data fetch ONLY after user actions (create/delete/update)
### Next Steps
-**Choose your approach:**
+1. **Complete collections fixes** (this guide)
+2. **Continue page-by-page review** - one page at a time
+3. **Reference `SSR_FIRST_ALPINE_GUIDE.md`** for SSR-first principles
+4. **Reference `ALPINE_COMPLETION_GUIDE.md`** for full Alpine.js migration (future goal)
-**If Path 1 (Quick Fixes):**
-1. Apply Option A to admin_library, dashboard, docs
-2. Continue page-by-page review
-3. Plan full Alpine.js migration for later
+### Architecture
-**If Path 2 (Full Alpine.js):**
-1. Follow ALPINE_COMPLETION_GUIDE.md completely
-2. Start with header.templ (reference implementation)
-3. Apply Alpine.store pattern to all modals
-4. Eliminate all manual DOM manipulation
+**State location:**
+- Templates: UI state (`x-data`, `x-show`)
+- Backend: SSR data
+- TypeScript: Business logic only
-**If Mixed Approach (Recommended):**
-1. Apply Option B (full Alpine.js) to admin_library
-2. Apply Option A (quick fix) to dashboard and docs
-3. Learn the pattern as you go
-4. Gradually migrate to full Alpine.js
-
-**Continue Page-by-Page Review:**
-- Complete collections page fixes
-- Review and fix remaining pages one at a time
-- Update this guide as each page is completed
+**No hybrid approach:**
+- ✅ Follow SSR-first principles
+- ✅ Full Alpine.js is eventual goal (see ALPINE_COMPLETION_GUIDE.md)
+- ✅ Fix issues methodically as you encounter them
diff --git a/SSR_FIRST_ALPINE_GUIDE.md b/SSR_FIRST_ALPINE_GUIDE.md
new file mode 100644
index 0000000..f2e8dc7
--- /dev/null
+++ b/SSR_FIRST_ALPINE_GUIDE.md
@@ -0,0 +1,627 @@
+# SSR-First Alpine.js Architecture Guide
+
+## Overview
+
+This guide ensures Alpine.js integration **maintains SSR-first architecture** while providing interactive features.
+
+**Goal:** Use Alpine.js for interactivity WITHOUT replacing SSR content on page load.
+
+**Complementary to:** `ALPINE_COMPLETION_GUIDE.md` (which covers eliminating manual DOM manipulation)
+
+---
+
+## Table of Contents
+
+1. [SSR-First Principles](#ssr-first-principles)
+2. [Page Type Classifications](#page-type-classifications)
+3. [The SSR Data Fetch Problem](#the-ssr-data-fetch-problem)
+4. [DOMContentLoaded Cleanup](#domcontentloaded-cleanup)
+5. [Page-by-Page Strategy](#page-by-page-strategy)
+6. [Authentication & SSR](#authentication--ssr)
+7. [Verification](#verification)
+
+---
+
+## SSR-First Principles
+
+### Core Rule
+
+**❌ NEVER fetch data in x-init if the data is already SSR'd**
+
+### What Each Layer Does
+
+| Layer | Responsibility |
+|-------|---------------|
+| **Backend (Go)** | SSR initial page load with real data |
+| **Template (.templ)** | Render SSR data, define UI state with `x-data` |
+| **Alpine.js** | Manage UI state (modals, dropdowns, transitions) |
+| **TypeScript** | Pure business logic (API calls, data processing) |
+
+### Three Page Types
+
+1. **80% SSR Pages** (most pages)
+ - Backend provides initial data
+ - Alpine handles modals/dropdowns only
+ - x-init NEVER fetches data
+
+2. **SSR + Interactive Pages** (dashboard, bookshelf)
+ - Backend provides initial data
+ - Alpine handles interactivity (drag-drop, filtering)
+ - x-init ONLY sets up event listeners, never fetches
+
+3. **80% JavaScript Pages** (analytics)
+ - Backend renders empty shell
+ - x-init fetches ALL data on page load
+ - Exception to the rule (intentional design)
+
+---
+
+## Page Type Classifications
+
+### Type 1: 80% SSR Pages (Most Pages)
+
+**Examples:** Collections, Conflicts, Queue, Devices, Profile
+
+**Characteristics:**
+- Full SSR data from backend
+- Alpine for modals/dropdowns only
+- No data fetch in x-init
+
+**Template Pattern:**
+```templ
+
+ @Header(user, currentPath)
+
+
+
{ collections }
+
+
+
+
+
Modal content
+
+
+```
+
+**TypeScript:**
+```typescript
+// Business logic only - no UI state
+async function deleteCollection(id: string) {
+ await apiDelete(`/collections/${id}`);
+}
+
+Alpine.data("collections", () => ({
+ deleteCollection, // Business logic only
+ // NO modal state - template handles it
+}));
+```
+
+### Type 2: SSR + Interactive Pages
+
+**Examples:** Dashboard, Bookshelf, Admin Library
+
+**Characteristics:**
+- Backend provides initial data
+- Alpine manages complex interactivity
+- x-init sets up event listeners ONLY
+
+**Dashboard Template Pattern:**
+```templ
+
+ @Header(user, "/dashboard")
+
+
+ { sections }
+
+
+
+
+
+
+
+```
+
+**Dashboard TypeScript:**
+```typescript
+// ❌ WRONG - fetches data on page load, replaces SSR
+function initDashboard() {
+ fetch('/api/dashboard/sections').then(renderDashboard);
+}
+
+// ✅ CORRECT - sets up event listeners only
+function initDashboard() {
+ initDragAndDrop(); // Setup event listeners
+ setupLibrarySelect(); // Setup event listener for library switching
+ // NO data fetch - SSR provides initial data
+}
+
+Alpine.data("dashboard", () => ({
+ initDashboard,
+}));
+```
+
+### Type 3: 80% JavaScript Pages (Exception)
+
+**Examples:** Analytics
+
+**Characteristics:**
+- Backend renders empty shell
+- x-init fetches ALL data
+- This is intentional - analytics is a dynamic dashboard
+
+**Analytics Template Pattern:**
+```templ
+
+ @Header(user, "/analytics")
+
+
+
+
+
+```
+
+**Analytics TypeScript:**
+```typescript
+// ✅ CORRECT - analytics is 80% JS by design
+async function loadAnalytics() {
+ const [statsRes, devicesRes] = await Promise.all([
+ fetch("/api/analytics/stats"),
+ fetch("/api/analytics/devices"),
+ ]);
+
+ renderReadingStats(await statsRes.json());
+ renderDeviceUsage(await devicesRes.json());
+}
+
+Alpine.data("analytics", () => ({
+ loadAnalytics,
+}));
+```
+
+---
+
+## The SSR Data Fetch Problem
+
+### The Bug
+
+**❌ BUG:** x-init fetches data and replaces SSR content
+
+```typescript
+// ❌ WRONG - replaces SSR content on page load
+function initializeAdmin() {
+ fetch('/api/libraries').then(renderLibraries); // BUG!
+}
+```
+
+```html
+
+
+
+ { libraries }
+
+
+
+```
+
+### The Fix
+
+**✅ Solution:** Remove data fetch from x-init
+
+```typescript
+// ✅ CORRECT - no data fetch
+function initializeAdmin() {
+ setupEventListeners(); // Setup only
+}
+
+// Keep fetch for AFTER CRUD operations
+async function reloadLibraries() {
+ fetch('/api/libraries').then(renderLibraries); // OK after create/delete
+}
+```
+
+```html
+
+
+
+ { libraries }
+
+
+
+```
+
+### When to Fetch Data
+
+✅ **OK to fetch in x-init:**
+- Analytics pages (Type 3)
+- Empty pages that need data
+- User-driven navigation (not initial page load)
+
+❌ **NOT OK to fetch in x-init:**
+- Pages with SSR data (Types 1 & 2)
+- Data that backend already provided
+- Replacing SSR content on page load
+
+✅ **OK to fetch AFTER user action:**
+- After create/delete/update operations
+- After dropdown selection
+- After form submission
+
+---
+
+## DOMContentLoaded Cleanup
+
+### Problem
+
+**DOMContentLoaded listeners run on EVERY page** due to `main.ts` importing all modules.
+
+**Example:**
+```typescript
+// web/src/admin.ts
+document.addEventListener("DOMContentLoaded", () => {
+ initializeAdmin(); // Runs on index page!
+});
+```
+
+```typescript
+// web/src/main.ts
+import "./admin"; // Imports admin module on ALL pages
+import "./dashboard"; // Imports dashboard module on ALL pages
+```
+
+### Solution: Two Approaches
+
+#### Approach 1: x-init Wrapper (Current Approach)
+
+Wrap DOMContentLoaded logic in named function, call via x-init:
+
+```typescript
+// web/src/admin.ts
+function initializeAdmin() {
+ setupEventListeners();
+}
+
+Alpine.data("admin", () => ({
+ initializeAdmin,
+}));
+```
+
+```html
+
+
+```
+
+#### Approach 2: Event Delegation Only (Future)
+
+Remove x-init entirely, rely on global event delegation:
+
+```typescript
+// web/src/admin.ts
+// NO init function - use global event delegation
+
+// Global event listener checks for data-action attributes
+document.addEventListener("click", (e) => {
+ const action = e.target.closest("[data-action]")?.dataset.action;
+ if (action === "delete-library") deleteLibrary();
+});
+```
+
+```html
+
+
+
+
+
+```
+
+### Which to Use?
+
+- **Current state:** Use Approach 1 (x-init wrapper)
+- **Future goal:** Use Approach 2 (event delegation only)
+- **Migration:** See `ALPINE_COMPLETION_GUIDE.md` for full migration path
+
+---
+
+## Page-by-Page Strategy
+
+### Dashboard
+
+**Type:** SSR + Interactive
+
+**Current Issues:**
+- Has DOMContentLoaded (needs wrapper)
+- Has localStorage redirect logic
+
+**Solution:**
+```typescript
+// ✅ Wrap existing logic in initDashboard()
+function initDashboard() {
+ initDragAndDrop(); // Setup drag-drop
+
+ document.addEventListener("click", handleDashboardClick); // Event delegation
+
+ // Check localStorage for saved library
+ const savedLibrary = localStorage.getItem("selectedLibrary");
+ if (savedLibrary && savedLibrary !== currentLibrary) {
+ window.location.href = `/dashboard?library_id=${savedLibrary}`;
+ }
+}
+
+Alpine.data("dashboard", () => ({
+ initDashboard,
+}));
+```
+
+```html
+
+
+```
+
+### Admin Library
+
+**Type:** SSR + Interactive
+
+**Current Issues:**
+- Has `x-init="initializeLibraryAdmin"` which calls `reloadLibraries()`
+- This fetches data and replaces SSR content
+
+**Solution:**
+```typescript
+// ❌ REMOVE this:
+function initializeLibraryAdmin() {
+ setupEventListeners();
+ void reloadLibraries(); // BUG - fetches data!
+}
+
+// ✅ CORRECT:
+function initializeLibraryAdmin() {
+ setupEventListeners(); // Setup only
+ // No data fetch
+}
+
+// Keep for after CRUD operations:
+async function reloadLibraries() {
+ const libraries = await apiGet("/libraries");
+ renderLibraries(libraries.data); // OK after create/delete
+}
+```
+
+### Collections
+
+**Type:** 80% SSR
+
+**Current State:** Dead exports removed (see `COLLECTIONS_CLEANUP_GUIDE.md`)
+
+**Solution:**
+- No x-init needed
+- Use Alpine.store for modal state (see `ALPINE_COMPLETION_GUIDE.md`)
+- Business logic functions only in TypeScript
+
+### Analytics
+
+**Type:** 80% JavaScript
+
+**Current State:** Already correct
+
+**Solution:**
+- Keep `x-init="loadAnalytics"`
+- Data fetch is intentional (analytics is dynamic)
+
+### Docs
+
+**Type:** 80% SSR
+
+**Current State:** Has DOMContentLoaded
+
+**Solution:**
+```typescript
+// ✅ Simple setup only
+function initializeDocsSearch() {
+ const searchInput = document.getElementById("docs-search");
+ searchInput?.addEventListener("input", handleDocsSearchInput);
+}
+
+Alpine.data("docs", () => ({
+ initializeDocsSearch,
+}));
+```
+
+```html
+
+
+```
+
+### Search (in Header)
+
+**Type:** Special case
+
+**Current Issues:**
+- Has DOMContentLoaded that runs on ALL pages (via main.ts import)
+- Uses localStorage for token (not SSR-friendly)
+
+**Future:** User plans to revamp search
+
+**Current Solution:**
+- Leave as-is for now
+- Revisit when search is redesigned
+- Consider moving token to SSR (see Authentication section below)
+
+---
+
+## Authentication & SSR
+
+### Problem: Token in localStorage
+
+```typescript
+// ❌ Current - client-side token
+const token = localStorage.getItem("token");
+fetch("/api/libraries", {
+ headers: { Authorization: `Bearer ${token}` },
+});
+```
+
+**Issues:**
+- Not SSR-friendly
+- Requires client-side storage
+- Fails if JS disabled
+
+### Solution: Server-Side Token Injection
+
+**Backend:** Extract token from HttpOnly cookie
+
+```go
+// internal/router/helpers.go
+func getTemplateUserWithTheme(c echo.Context, cfg *config.Config) (templates.User, error) {
+ user := getUserFromSession(c)
+
+ // Extract JWT from HttpOnly cookie
+ token := ""
+ for _, cookie := range c.Cookies() {
+ if cookie.Name == "token" {
+ token = cookie.Value
+ break
+ }
+ }
+
+ return templates.User{
+ ID: user.ID,
+ Username: user.Username,
+ Token: token, // Add token to user struct
+ Theme: user.Theme,
+ }, nil
+}
+```
+
+**Template:** Inject token into WebSocket URL
+
+```templ
+
+
+```
+
+**Benefits:**
+- ✅ SSR-compatible
+- ✅ No localStorage needed
+- ✅ Works with HttpOnly cookies
+- ✅ More secure
+
+---
+
+## Verification
+
+### Checklist for Each Page
+
+**Type 1 (80% SSR):**
+- [ ] Backend provides all data
+- [ ] x-init does NOT fetch data
+- [ ] x-init only sets up event listeners (if needed)
+- [ ] Modals use Alpine.store or local x-data
+
+**Type 2 (SSR + Interactive):**
+- [ ] Backend provides initial data
+- [ ] x-init does NOT fetch data on page load
+- [ ] Data fetch only after user action (dropdown change, button click)
+- [ ] Event listeners set up in x-init
+
+**Type 3 (80% JS):**
+- [ ] Backend renders empty shell
+- [ ] x-init fetches ALL data on page load
+- [ ] This is intentional and documented
+
+### Testing
+
+```bash
+# 1. Start application
+go run .
+
+# 2. Open browser DevTools
+# Network tab -> Disable cache
+
+# 3. Load dashboard
+# Expected: /dashboard HTML response contains full data
+# Expected: NO /api/dashboard/sections call on page load
+
+# 4. Change library dropdown
+# Expected: /api/dashboard/sections?library_id=XXX call
+# Expected: Content updates
+
+# 5. Load analytics
+# Expected: /analytics HTML response is empty shell
+# Expected: /api/analytics/stats call on page load
+# Expected: /api/analytics/devices call on page load
+```
+
+### Common Bugs to Check
+
+**❌ SSR content flashes, then gets replaced:**
+- x-init is fetching data
+- Remove data fetch from x-init
+
+**❌ API call on page load for SSR page:**
+- x-init or DOMContentLoaded is calling fetch
+- Move fetch to after user action
+
+**❌ Modal not opening:**
+- Missing x-data wrapper
+- Check Alpine DevTools for state
+
+**❌ Search not working:**
+- Check if search input has id="header-search"
+- Check if initializeSearch() is called
+
+---
+
+## Summary
+
+### Key Rules
+
+1. **State lives in template** (`x-data`, `x-show`)
+2. **UI updates automatically** (Alpine reactivity)
+3. **No manual DOM manipulation** in TypeScript
+4. **Pure business logic** in TypeScript functions
+5. **❌ NEVER fetch data in x-init** if data is SSR'd
+6. **✅ OK to fetch** after user action or for Type 3 pages
+
+### Architecture
+
+```
+┌─────────────┐
+│ Backend │ SSR data
+│ (Go) │────────────┐
+└─────────────┘ │
+ ▼
+ ┌──────────┐
+ │ Template │ x-data state
+ │ (.templ) │────────────┐
+ └──────────┘ │
+ ▼
+ ┌──────────────┐
+ │ Alpine.js │ UI state
+ │ (Reactivity) │
+ └──────────────┘
+ ▲
+ │
+┌─────────────┐ Business Logic ┌─────────────┐
+│ TypeScript │─────────────────│ Browser APIs │
+│ (API only) │ │ (fetch, etc) │
+└─────────────┘ └─────────────┘
+```
+
+### Related Guides
+
+- **`ALPINE_COMPLETION_GUIDE.md`** - Eliminate manual DOM manipulation
+- **`COLLECTIONS_CLEANUP_GUIDE.md`** - Fix dead exports and DOMContentLoaded issues
+- **`PROJECT_GUIDELINES.md`** - Project architecture standards
+
+---
+
+## Next Steps
+
+1. **Classify each page** as Type 1, 2, or 3
+2. **Remove data fetches** from x-init on Type 1 & 2 pages
+3. **Move token to SSR** (Authentication section)
+4. **Test each page** to verify SSR content is not replaced
+5. **Document exceptions** (Type 3 pages)
+
+**Remember:** SSR-first means backend provides the truth, Alpine handles the interaction.