# Alpine.js and SSR Architecture Cleanup Guide ## Overview This guide provides a systematic, page-by-page approach to fixing Alpine.js issues while maintaining SSR-first architecture. ### Problem Statement Console errors caused by: 1. Dead Alpine.js exports (functions that don't exist) 2. DOMContentLoaded listeners running on wrong pages 3. Missing x-init calls in templates 4. **CRITICAL:** x-init functions that fetch data and replace SSR content ### SSR-First Alpine.js Strategy **Different pages have different SSR/JavaScript ratios:** - **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 - **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 - **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 - **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 **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 ### 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 **Goal:** Clean, working Alpine.js integration with proper SSR architecture. --- ## Prerequisites Before starting, verify the current state: ```bash # Check current errors cd /home/nymusicman/Code/bookhoard npm run build:ts # Should see errors about: # - "addbooksToAdd is not defined" # - "removebooksToAdd is not defined" # - "toggleBookSelection is not defined" # - etc. ``` --- ## Step 1: Fix collections.ts Alpine.data Export **File:** `web/src/collections.ts` **Problem:** Alpine.data exports functions that were deleted in commit 93710a1. **Action:** Update the Alpine.data export to only include existing functions. ### Step 1.1: Read Current Alpine.data Export ```bash # Check what's currently exported tail -50 web/src/collections.ts | grep -A25 "Alpine.data" ``` You'll see something like: ```typescript Alpine.data("collections", () => ({ addbooksToAdd, // ❌ Does NOT exist - deleted backToCollections, closeCollectionModal, createRule, deleteRule, filterCollectionBooks, filterIcons, hideAddBooksModal, initCollectionDetail, // ❌ Does NOT exist - deleted initColorSelection, // ❌ Does NOT exist - deleted initIconSelection, // ❌ Does NOT exist - deleted loadCollectionRules, loadCollections, navigateToCollection, populateIconGrid, removeBook, removebooksToAdd, // ❌ Does NOT exist - deleted searchBooksForCollections, selectColor, selectIcon, showAddBooksModal, showAllIcons, // ❌ Does NOT exist - deleted setupHTMXAuth, testRule, toggleBookForRemoval, // ❌ Does NOT exist - deleted toggleBookSelection, // ❌ Does NOT exist - deleted updateSelectedCount, })); ``` ### Step 1.2: Read Current Export Statement ```bash # Check the export statement at the end of the file grep -A30 "^export {" web/src/collections.ts ``` You'll see similar dead exports. ### Step 1.3: Verify Which Functions Actually Exist ```bash # Search for function definitions grep -n "^function\|^async function" web/src/collections.ts ``` Expected output (actual existing functions): - `backToCollections` ✓ - `closeCollectionModal` ✓ - `createRule` ✓ - `deleteRule` ✓ - `filterCollectionBooks` ✓ - `filterIcons` ✓ - `hideAddBooksModal` ✓ - `loadCollectionRules` ✓ - `loadCollections` ✓ - `navigateToCollection` ✓ - `populateIconGrid` ✓ - `removeBook` ✓ - `searchBooksForCollections` ✓ - `selectColor` ✓ - `selectIcon` ✓ - `showAddBooksModal` ✓ - `setupHTMXAuth` ✓ - `testRule` ✓ - `updateSelectedCount` ✓ ### Step 1.4: Update the Export Statement **Line 407:** Change the `export` statement to only include existing functions: ```typescript export { backToCollections, closeCollectionModal, createRule, deleteRule, filterCollectionBooks, filterIcons, hideAddBooksModal, loadCollectionRules, loadCollections, navigateToCollection, populateIconGrid, removeBook, searchBooksForCollections, selectColor, selectIcon, showAddBooksModal, setupHTMXAuth, testRule, updateSelectedCount, }; ``` ### Step 1.5: Update Alpine.data Registration **Line 424:** Update Alpine.data to match the export: ```typescript Alpine.data("collections", () => ({ backToCollections, closeCollectionModal, createRule, deleteRule, filterCollectionBooks, filterIcons, hideAddBooksModal, loadCollectionRules, loadCollections, navigateToCollection, populateIconGrid, removeBook, searchBooksForCollections, selectColor, selectIcon, showAddBooksModal, setupHTMXAuth, testRule, updateSelectedCount, })); ``` ### Step 1.6: Verify the Fix ```bash # Build TypeScript npm run build:ts # Should now succeed with 0 errors ``` --- ## Step 2: Fix Template Function Calls **File:** `templates/collections.templ` **Problem:** Template calls functions that no longer exist. ### Step 2.1: Remove Dead Function Calls from Collection Detail Page **Line 226:** Remove the `removebooksToAdd` call: ```html ``` **Line 229:** Remove `addbooksToAdd` call: ```html ``` ### Step 2.2: Regenerate Templates ```bash # Generate Go template files templ generate # Should see: Complete [updates=0 duration=~40ms] ``` --- ## Step 3: Fix Other TypeScript Files (Remove DOMContentLoaded) For each file, we'll remove the `DOMContentLoaded` listener and add x-init to the template. ### Step 3.1: analytics.ts **File:** `web/src/analytics.ts` **Current:** ```typescript export { loadAnalytics }; document.addEventListener("DOMContentLoaded", loadAnalytics); Alpine.data("analytics", () => ({ loadAnalytics, })); ``` **Change to:** ```typescript export { loadAnalytics }; // REMOVE this line: // document.addEventListener("DOMContentLoaded", loadAnalytics); Alpine.data("analytics", () => ({ loadAnalytics, })); ``` **Template Update:** `templates/analytics.templ` **Line 14:** Add x-init to body tag: ```html ``` ### Step 3.2: docs.ts **File:** `web/src/docs.ts` **Current (around line 95-100):** ```typescript document.addEventListener("DOMContentLoaded", () => { initializeDocsSearch(); }); export { toggleSidebar, initializeDocsSearch }; Alpine.data("docs", () => ({ toggleSidebar, initializeSearch: initializeDocsSearch, })); ``` **Change to:** ```typescript // REMOVE this entire block: // document.addEventListener("DOMContentLoaded", () => { // initializeDocsSearch(); // }); export { toggleSidebar, initializeDocsSearch }; Alpine.data("docs", () => ({ toggleSidebar, initializeSearch: initializeDocsSearch, })); ``` **Template Update:** `templates/docs.templ` **Find the `` tag** and add x-init: ```html ``` ### Step 3.3: library.ts - SSR-FIRST FIX REQUIRED **⚠️ CRITICAL SSR BUG:** The `initializeLibraryAdmin()` function currently calls `reloadLibraries()` which fetches data from the API and replaces the SSR-rendered library list. This defeats SSR for the admin library page! **File:** `web/src/library.ts` **Problem (around line 653-655):** ```typescript function initializeLibraryAdmin(): void { // Setup event listeners const librariesList = document.getElementById("libraries-list"); if (librariesList) { librariesList.addEventListener("click", handleLibraryListClick); } // ... more setup ... // ❌ BUG: This fetches data and replaces SSR content on page load! void reloadLibraries(); } ``` **Fix - Remove data fetch from init:** ```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 } ``` **Why this fix matters:** - SSR renders the library list on the server for fast initial load - Calling `reloadLibraries()` in x-init replaces this with a slower API call - `reloadLibraries()` should ONLY be called after CRUD operations (create/delete/update) - The `initializeLibraryAdmin()` should ONLY setup event listeners and modals **Template Update:** `templates/admin_library.templ` **Current state (line 11):** ```html ``` **Status:** ✅ Already correct - template already has x-data and x-init setup **Action:** No template change needed - just fix library.ts to not fetch data in init ### Step 3.4: dashboard.ts **File:** `web/src/dashboard.ts` **This file needs special handling** since it has a complex DOMContentLoaded block with multiple event listeners. **Current (lines 494-521):** ```typescript document.addEventListener("DOMContentLoaded", () => { initDragAndDrop(); document.addEventListener("click", (e: Event) => { // ... 70+ lines of event handling ... }); document.addEventListener("input", (e: Event) => { // ... event handling ... }); // ... more initialization ... }); ``` **Create a wrapper function at the end of the file:** **Add before the export statement (before line ~490):** ```typescript // Wrapper function for dashboard initialization function initDashboard() { initDragAndDrop(); document.addEventListener("click", (e: Event) => { const target = e.target as HTMLElement; const actionElem = target.closest("[data-action]") as HTMLElement; const action = actionElem?.getAttribute("data-action"); switch (action) { case "scroll-carousel": { const collectionId = target.dataset.direction || actionElem?.dataset.direction || "0"; if (collectionId) scrollCarousel(collectionId, direction); break; } // ... keep all existing cases ... } }); document.addEventListener("input", (e: Event) => { const target = e.target as HTMLElement; const actionElem = target.closest("[data-input-action]") as HTMLElement; const action = actionElem?.getAttribute("data-input-action"); switch (action) { case "update-items-count": { const input = target as HTMLInputElement; const displayTarget = input.getAttribute("target"); if (displayTarget) updateItemsCount(input, displayTarget); break; } } }); // Load saved library on page load const librarySelect = document.getElementById( "library-select", ) as HTMLSelectElement; if (librarySelect) { librarySelect.addEventListener("change", (e) => { const target = e.target as HTMLSelectElement; if (target.value) { switchLibrary(target.value); } }); } 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}`; } } ``` **Remove the old DOMContentLoaded block** (delete lines 494-521): ```typescript // DELETE this entire block: // document.addEventListener("DOMContentLoaded", () => { // initDragAndDrop(); // ... all 70+ lines ... // }); ``` **Add to export statement:** ```typescript export { closeDashboardSettings, openDashboardSettings, initDashboard, // ← ADD THIS saveDashboardSettings, scrollCarousel, // ... keep all other exports ... }; ``` **Add to Alpine.data:** ```typescript Alpine.data("dashboard", () => ({ closeDashboardSettings, openDashboardSettings, initDashboard, // ← ADD THIS saveDashboardSettings, scrollCarousel, // ... keep all other exports ... })); ``` **Template Update:** `templates/dashboard.templ` **Line 19:** Add x-data and x-init to body tag: ```html ``` ### Step 3.5: Verify All TypeScript Files ```bash # Build all TypeScript npm run build:ts # Should succeed with 0 errors ``` --- ## Step 4: Regenerate Templates ```bash # Generate Go template files templ generate # Expected: Complete [updates=X duration=~40ms] # where X is the number of templates modified ``` --- ## Step 5: Build and Verify ```bash # Build the Go server go build ./cmd/server # If build succeeds, you're done! # If build fails, check the error and fix accordingly ``` --- ## Step 6: Test in Browser 1. **Start the server:** ```bash podman compose up -d ``` 2. **Open browser and test:** - Navigate to `/collections` - should work with no console errors - Navigate to `/analytics` - should load analytics data - Navigate to `/dashboard` - drag and drop should work - Navigate to `/docs` - search should work - Navigate to `/admin/library` - library management should work 3. **Check browser console:** - No "X is not defined" errors - No "n.bind is not a function" errors - No "initX is not defined" errors --- ## Summary ### Progress So Far **Pages methodically reviewed:** - ✅ **Dashboard** - Completed (drag-drop works, SSR maintained) - 🔄 **Collections** - In progress (dead exports removed, more fixes needed) - ⏸️ **Admin Library** - SSR bug identified, fix documented in Step 3.3 - ⏸️ **All other pages** - Not yet reviewed, will be done page-by-page ### Completed Changes **TypeScript Files (3 files):** - `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 **Template Files (1 file):** - `templates/analytics.templ` - ✅ Added x-init="loadAnalytics" (JS-heavy page, correct) ### Pending Critical Fixes **SSR Bug in library.ts (Step 3.3):** - ❌ `initializeLibraryAdmin()` calls `reloadLibraries()` which fetches data and replaces SSR - ✅ Fix documented: Remove data fetch from init, only setup event listeners - ⚠️ Template already correct: `templates/admin_library.templ` has x-data/x-init ### What Was NOT Changed - **"Add Books" modal** - Still client-side Alpine.js (quick fix decision) - **WebSocket code** - Already moved to templates with server-side token injection - **API endpoints** - Already exist and work correctly - **HTMX modals** - Already implemented for collection CRUD - **Search** - Skipped pending user's planned revamp ### SSR-First Principles Applied ✅ **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:** Will fix to only setup event listeners, SSR provides data ✅ **Collections:** SSR provides data, JS for interactivity (in progress) ### Result So Far ✅ Collections dead exports removed ✅ Analytics loads correctly with SSR-first approach ✅ Admin WebSocket fixed with server-side token injection ⏳ Admin library SSR bug identified, fix ready to implement ⏳ Other pages will be reviewed methodically, one by one ### Next Steps **Immediate (Critical SSR Bug):** 1. Fix `web/src/library.ts` Step 3.3 - Remove `reloadLibraries()` from `initializeLibraryAdmin()` 2. Test admin library page to verify SSR content is not replaced **Continue Page-by-Page Review:** 3. Complete collections page fixes 4. Review and fix remaining pages one at a time 5. Update this guide as each page is completed