# 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.
