docs(ssr): create SSR-first Alpine.js guide and update cleanup guide
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.
This commit is contained in:
+113
-441
@@ -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", () => ({
|
||||
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">
|
||||
```
|
||||
|
||||
### 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", () => ({
|
||||
<body x-data="docs" x-init="initializeDocsSearch" class="theme-{ user.Theme }">
|
||||
```
|
||||
|
||||
**✅ 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
|
||||
<!-- templates/docs.templ -->
|
||||
<body class="theme-{ user.Theme }">
|
||||
<!-- No x-init needed -->
|
||||
```
|
||||
|
||||
**✅ 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
|
||||
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
|
||||
```
|
||||
|
||||
**✅ 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
|
||||
<div id="create-library-modal" class="hidden fixed inset-0 z-50">
|
||||
<div class="card rounded-lg p-6">
|
||||
<h2>Create Library</h2>
|
||||
<button data-action="hide-create-modal">✕</button>
|
||||
<form id="create-library-form">...</form>
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Change to:**
|
||||
```html
|
||||
<!-- Wrap page in x-data with modal state -->
|
||||
<div x-data="{
|
||||
createLibraryModalOpen: false,
|
||||
deleteLibraryModalOpen: false,
|
||||
folderBrowserModalOpen: false
|
||||
}">
|
||||
|
||||
<!-- Keep existing content, but update modal: -->
|
||||
|
||||
<div x-show="createLibraryModalOpen"
|
||||
x-transition
|
||||
@click.self="createLibraryModalOpen = false"
|
||||
class="fixed inset-0 z-50 flex items-center justify-center"
|
||||
style="background-color: rgba(0, 0, 0, 0.7); display: none;">
|
||||
<div @click.stop class="card rounded-lg p-6 w-full max-w-md mx-4">
|
||||
<div class="flex justify-between items-center mb-6">
|
||||
<h2 class="text-xl font-bold">Create Library</h2>
|
||||
<button @click="createLibraryModalOpen = false" class="p-2">✕</button>
|
||||
</div>
|
||||
<form id="create-library-form">...</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
```
|
||||
|
||||
**Update button to open modal (line 22):**
|
||||
```html
|
||||
<!-- Before -->
|
||||
<button data-action="show-create-modal">+ Create Library</button>
|
||||
|
||||
<!-- After -->
|
||||
<button @click="createLibraryModalOpen = true">+ Create Library</button>
|
||||
```
|
||||
|
||||
**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
|
||||
<!-- BEFORE -->
|
||||
<body x-data="library" x-init="initializeLibraryAdmin" class="theme-{ user.Theme }">
|
||||
|
||||
<!-- AFTER -->
|
||||
<body class="theme-{ user.Theme }">
|
||||
```
|
||||
|
||||
**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", () => ({
|
||||
<body x-data="dashboard" x-init="initDashboard" class="theme-{ user.Theme }">
|
||||
```
|
||||
|
||||
**✅ 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
|
||||
<!-- templates/dashboard.templ line 19 -->
|
||||
<body class="theme-{ user.Theme }">
|
||||
<!-- No x-data or x-init needed -->
|
||||
```
|
||||
|
||||
**✅ 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
|
||||
|
||||
@@ -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
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, currentPath)
|
||||
|
||||
<!-- SSR data rendered here -->
|
||||
<div>{ collections }</div>
|
||||
|
||||
<!-- Alpine manages modal state only -->
|
||||
<div x-data="{ modalOpen: false }">
|
||||
<button @click="modalOpen = true">Open</button>
|
||||
<div x-show="modalOpen" x-transition>Modal content</div>
|
||||
</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
**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
|
||||
<body class="theme-{ user.Theme }">
|
||||
@Header(user, "/dashboard")
|
||||
|
||||
<!-- SSR data from backend -->
|
||||
{ sections }
|
||||
|
||||
<!-- Alpine handles drag-drop, library switching -->
|
||||
<div x-data="dashboard">
|
||||
<!-- Drag-drop areas -->
|
||||
<!-- Library selector (triggers data fetch on change, not init) -->
|
||||
</div>
|
||||
</body>
|
||||
```
|
||||
|
||||
**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
|
||||
<body x-data="analytics" x-init="loadAnalytics" class="theme-{ user.Theme }">
|
||||
@Header(user, "/analytics")
|
||||
|
||||
<!-- Empty containers - JavaScript fills them -->
|
||||
<div id="reading-stats"></div>
|
||||
<div id="device-usage"></div>
|
||||
</body>
|
||||
```
|
||||
|
||||
**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
|
||||
<!-- ❌ WRONG - SSR content gets replaced -->
|
||||
<body x-data="library" x-init="initializeAdmin">
|
||||
<!-- Backend rendered this: -->
|
||||
{ libraries }
|
||||
|
||||
<!-- But x-init fetches and replaces it! -->
|
||||
</body>
|
||||
```
|
||||
|
||||
### 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
|
||||
<!-- ✅ CORRECT - SSR content stays -->
|
||||
<body x-data="library" x-init="initializeAdmin">
|
||||
<!-- Backend rendered this, it stays: -->
|
||||
{ libraries }
|
||||
|
||||
<!-- x-init only sets up event listeners -->
|
||||
</body>
|
||||
```
|
||||
|
||||
### 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
|
||||
<!-- templates/admin.templ -->
|
||||
<body x-data="admin" x-init="initializeAdmin">
|
||||
```
|
||||
|
||||
#### 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
|
||||
<!-- templates/admin.templ -->
|
||||
<body>
|
||||
<!-- No x-init needed -->
|
||||
<button data-action="delete-library">Delete</button>
|
||||
</body>
|
||||
```
|
||||
|
||||
### 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
|
||||
<!-- templates/dashboard.templ -->
|
||||
<body x-data="dashboard" x-init="initDashboard" class="theme-{ user.Theme }">
|
||||
```
|
||||
|
||||
### 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
|
||||
<!-- templates/docs.templ -->
|
||||
<body x-data="docs" x-init="initializeDocsSearch" class="theme-{ user.Theme }">
|
||||
```
|
||||
|
||||
### 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
|
||||
<!-- templates/admin.templ -->
|
||||
<script>
|
||||
const ws = new WebSocket(`ws://localhost:8765/ws/sync?token={ user.Token }`);
|
||||
</script>
|
||||
```
|
||||
|
||||
**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.
|
||||
Reference in New Issue
Block a user