From ab2e2427ccfc9fab32cf352038d11368d2435757 Mon Sep 17 00:00:00 2001 From: John O'Keefe Date: Thu, 12 Mar 2026 18:07:16 -0400 Subject: [PATCH] docs(ssr): create SSR-first Alpine.js guide and update cleanup guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Created comprehensive SSR_FIRST_ALPINE_GUIDE.md to establish SSR-first architecture principles for Alpine.js integration. New Guide: SSR_FIRST_ALPINE_GUIDE.md Covers: - SSR-first principles (state in templates, no fetch in x-init for SSR pages) - Three page type classifications: * Type 1: 80% SSR (Collections, Conflicts) - backend provides all data * Type 2: SSR + Interactive (Dashboard, Admin Library) - SSR + interactivity * Type 3: 80% JavaScript (Analytics) - x-init fetches all data (intentional) - The SSR data fetch problem (x-init replacing SSR content) - DOMContentLoaded cleanup strategies - Page-by-page strategy for each type - Authentication & SSR (server-side token injection) - Verification checklist and testing approach - Architecture diagram showing data flow Key Principles: - ❌ 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 - ✅ State lives in template (x-data), not TypeScript Updated: COLLECTIONS_CLEANUP_GUIDE.md Changes: - Added reference to SSR_FIRST_ALPINE_GUIDE.md as authority - Removed two-option approach (no more choices) - Documented that admin library SSR bug is already fixed (commit 1b9bc64) - Simplified dashboard approach (wrap existing code in initDashboard) - Simplified docs approach (simple setup, no data fetch) - Updated summary to reflect completed work - Added architecture section showing state location Architecture Clarity: - Templates: UI state (x-data, x-show) - Backend: SSR data - TypeScript: Business logic only - No hybrid approach - follow SSR-first principles References: - SSR_FIRST_ALPINE_GUIDE.md - Complete SSR-first architecture - ALPINE_COMPLETION_GUIDE.md - Full Alpine.js migration (future goal) - PROJECT_GUIDELINES.md - Project standards This establishes a single source of truth for SSR-first Alpine.js architecture and removes confusion about which approach to use. --- COLLECTIONS_CLEANUP_GUIDE.md | 554 +++++++------------------------ SSR_FIRST_ALPINE_GUIDE.md | 627 +++++++++++++++++++++++++++++++++++ 2 files changed, 740 insertions(+), 441 deletions(-) create mode 100644 SSR_FIRST_ALPINE_GUIDE.md 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 - -``` - -**Change to:** -```html - -
- - - - - -
-``` - -**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.