CRITICAL FIX: initializeLibraryAdmin() was calling reloadLibraries()
which fetched data from the API and replaced the SSR-rendered library
list on page load, defeating the purpose of server-side rendering.
Changes in web/src/library.ts:
- Remove DOMContentLoaded listener (now uses Alpine x-init in template)
- Remove void reloadLibraries() call from initializeLibraryAdmin()
- Add comment explaining SSR provides initial data
- Add initializeLibraryAdmin to export statement
- Add initializeLibraryAdmin to Alpine.data() registration
- Keep reloadLibraries() as standalone function for use after CRUD ops
Rationale:
- SSR provides fast initial page load with library list
- x-init should ONLY setup event listeners, not fetch data
- reloadLibraries() is called after create/delete/update operations
- Follows SSR-first architecture: different pages have different
SSR/JS ratios (analytics is 80% JS, most pages are 80% SSR)
Documentation:
- Update COLLECTIONS_CLEANUP_GUIDE.md with SSR-first strategy
- Document page-by-page review status (dashboard ✓, collections 🔄)
- Fix template references (library.templ → admin_library.templ)
- Explain why analytics fetches data (intentional for dynamic page)
This ensures the admin library page maintains SSR benefits while
still providing interactive features via Alpine.js.
17 KiB
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:
- Dead Alpine.js exports (functions that don't exist)
- DOMContentLoaded listeners running on wrong pages
- Missing x-init calls in templates
- 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 callingreloadLibraries()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:
# 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
# Check what's currently exported
tail -50 web/src/collections.ts | grep -A25 "Alpine.data"
You'll see something like:
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
# 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
# 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:
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:
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
# 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:
<!-- BEFORE -->
<button
id="bulk-remove-btn"
@click="removebooksToAdd"
disabled
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
🗑️ Remove Selected
</button>
<!-- AFTER -->
<button
id="bulk-remove-btn"
disabled
class="btn-danger px-4 py-2 rounded-lg disabled:opacity-50 disabled:cursor-not-allowed"
>
🗑️ Remove Selected
</button>
Line 229: Remove addbooksToAdd call:
<!-- BEFORE -->
<button @click="addbooksToAdd" class="btn-primary px-4 py-2 rounded-lg">
➕ Add Selected Books
</button>
<!-- AFTER -->
<button type="button" class="btn-primary px-4 py-2 rounded-lg" style="opacity: 0.5; cursor: not-allowed;" disabled>
➕ Add Selected Books
</button>
Step 2.2: Regenerate Templates
# 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:
export { loadAnalytics };
document.addEventListener("DOMContentLoaded", loadAnalytics);
Alpine.data("analytics", () => ({
loadAnalytics,
}));
Change to:
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:
<!-- BEFORE -->
<body x-data="analytics" class="theme-{ user.Theme }">
<!-- AFTER -->
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">
Step 3.2: docs.ts
File: web/src/docs.ts
Current (around line 95-100):
document.addEventListener("DOMContentLoaded", () => {
initializeDocsSearch();
});
export { toggleSidebar, initializeDocsSearch };
Alpine.data("docs", () => ({
toggleSidebar,
initializeSearch: initializeDocsSearch,
}));
Change to:
// REMOVE this entire block:
// document.addEventListener("DOMContentLoaded", () => {
// initializeDocsSearch();
// });
export { toggleSidebar, initializeDocsSearch };
Alpine.data("docs", () => ({
toggleSidebar,
initializeSearch: initializeDocsSearch,
}));
Template Update: templates/docs.templ
Find the <body> tag and add x-init:
<!-- BEFORE -->
<body x-data="docs" class="theme-{ user.Theme }">
<!-- AFTER -->
<body x-data="docs" x-init="initializeSearch" class="theme-{ user.Theme }">
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):
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:
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):
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
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):
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):
// 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):
// DELETE this entire block:
// document.addEventListener("DOMContentLoaded", () => {
// initDragAndDrop();
// ... all 70+ lines ...
// });
Add to export statement:
export {
closeDashboardSettings,
openDashboardSettings,
initDashboard, // ← ADD THIS
saveDashboardSettings,
scrollCarousel,
// ... keep all other exports ...
};
Add to Alpine.data:
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:
<!-- BEFORE -->
<body class="theme-{ user.Theme }">
<!-- AFTER -->
<body class="theme-{ user.Theme }" x-data="dashboard" x-init="initDashboard()">
Step 3.5: Verify All TypeScript Files
# Build all TypeScript
npm run build:ts
# Should succeed with 0 errors
Step 4: Regenerate Templates
# 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
# 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
-
Start the server:
podman compose up -d -
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
- Navigate to
-
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 exportsweb/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()callsreloadLibraries()which fetches data and replaces SSR - ✅ Fix documented: Remove data fetch from init, only setup event listeners
- ⚠️ Template already correct:
templates/admin_library.templhas 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):
- Fix
web/src/library.tsStep 3.3 - RemovereloadLibraries()frominitializeLibraryAdmin() - 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