docs: add frontend data loading patterns and architecture (#232)

Documents the shift from client-side onMount to server-side load functions:
- server-side loading (+page.server.ts) for SEO/performance
- client-side loading (+page.ts) for auth-dependent data
- layout loading (+layout.ts) for shared state
- when to use each pattern
- anti-patterns to avoid
- migration history from PRs #210, #227

References issue #225 (auto-play investigation) as example of
understanding client vs server state boundaries.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude <noreply@anthropic.com>

authored by zzstoatzz.io Claude and committed by GitHub 45c9b1d1 d427db42

Changed files
+203
docs
frontend
+203
docs/frontend/data-loading.md
··· 1 + # data loading 2 + 3 + ## overview 4 + 5 + plyr.fm uses SvelteKit's server-side and client-side load functions for optimal performance and SEO. 6 + 7 + ## patterns 8 + 9 + ### server-side loading (`+page.server.ts`) 10 + 11 + used for: 12 + - SEO-critical pages (artist profiles, album pages) 13 + - data needed before page renders 14 + - public data that doesn't require client auth 15 + 16 + ```typescript 17 + // frontend/src/routes/u/[handle]/+page.server.ts 18 + export const load: PageServerLoad = async ({ params, fetch }) => { 19 + const artistResponse = await fetch(`${API_URL}/artists/by-handle/${params.handle}`); 20 + const artist = await artistResponse.json(); 21 + 22 + // parallel fetches for related data 23 + const [tracks, albums] = await Promise.all([ 24 + fetch(`${API_URL}/tracks/?artist_did=${artist.did}`).then(r => r.json()), 25 + fetch(`${API_URL}/albums/${params.handle}`).then(r => r.json()) 26 + ]); 27 + 28 + return { artist, tracks, albums }; 29 + }; 30 + ``` 31 + 32 + **benefits**: 33 + - data loads in parallel on server before page renders 34 + - eliminates client-side waterfall (sequential fetches) 35 + - proper HTTP status codes (404, 500) instead of loading states 36 + - meta tags populated with real data for link previews 37 + - faster perceived performance 38 + 39 + **example improvement**: 40 + - before: artist page loaded in ~1.66s (sequential client fetches) 41 + - after: instant render with server-loaded data 42 + 43 + ### client-side loading (`+page.ts`) 44 + 45 + used for: 46 + - auth-dependent data (liked tracks, user preferences) 47 + - data that needs client context (localStorage, cookies) 48 + - progressive enhancement 49 + 50 + ```typescript 51 + // frontend/src/routes/liked/+page.ts 52 + export const load: PageLoad = async ({ fetch }) => { 53 + const sessionId = localStorage.getItem('session_id'); 54 + if (!sessionId) return { tracks: [] }; 55 + 56 + const response = await fetch(`${API_URL}/tracks/liked`, { 57 + headers: { 'Authorization': `Bearer ${sessionId}` } 58 + }); 59 + 60 + return { tracks: await response.json() }; 61 + }; 62 + ``` 63 + 64 + **benefits**: 65 + - access to browser APIs (localStorage, cookies) 66 + - runs on client, can use session tokens 67 + - still loads before component mounts (faster than `onMount`) 68 + 69 + ### layout loading (`+layout.ts`) 70 + 71 + used for: 72 + - auth state (shared across all pages) 73 + - global data needed everywhere 74 + 75 + ```typescript 76 + // frontend/src/routes/+layout.ts 77 + export async function load({ fetch }: LoadEvent) { 78 + const sessionId = localStorage.getItem('session_id'); 79 + if (!sessionId) return { user: null, isAuthenticated: false }; 80 + 81 + const response = await fetch(`${API_URL}/auth/me`, { 82 + headers: { 'Authorization': `Bearer ${sessionId}` } 83 + }); 84 + 85 + if (response.ok) { 86 + return { user: await response.json(), isAuthenticated: true }; 87 + } 88 + 89 + return { user: null, isAuthenticated: false }; 90 + } 91 + ``` 92 + 93 + **benefits**: 94 + - loads once for entire app 95 + - shared data available to all child routes 96 + - eliminates duplicate auth checks on every page 97 + 98 + ## anti-patterns 99 + 100 + ### ❌ using `onMount` for initial data 101 + 102 + ```typescript 103 + // BAD - causes flash of loading, SEO issues 104 + let data = $state(null); 105 + 106 + onMount(async () => { 107 + const response = await fetch('/api/data'); 108 + data = await response.json(); 109 + }); 110 + ``` 111 + 112 + **problems**: 113 + - page renders empty, then populates (flash of loading) 114 + - data not available for SSR/link previews 115 + - slower perceived performance 116 + - sequential waterfalls when multiple fetches needed 117 + 118 + ### ✅ use load functions instead 119 + 120 + ```typescript 121 + // GOOD - data ready before render 122 + export const load: PageLoad = async ({ fetch }) => { 123 + return { data: await fetch('/api/data').then(r => r.json()) }; 124 + }; 125 + ``` 126 + 127 + ## when to use what 128 + 129 + | use case | pattern | file | 130 + |----------|---------|------| 131 + | public data, SEO critical | server load | `+page.server.ts` | 132 + | auth-dependent data | client load | `+page.ts` | 133 + | global shared data | layout load | `+layout.ts` | 134 + | real-time updates | state manager | `lib/*.svelte.ts` | 135 + | form submissions | server actions | `+page.server.ts` | 136 + | progressive enhancement | `onMount` | component | 137 + 138 + ## migration history 139 + 140 + ### november 2025 - server-side data loading shift 141 + 142 + **PR #210**: centralized auth and client-side load functions 143 + - added `+layout.ts` for auth state 144 + - added `+page.ts` to liked tracks page 145 + - centralized auth manager in `lib/auth.svelte.ts` 146 + 147 + **PR #227**: artist pages moved to server-side loading 148 + - replaced client `onMount` fetches with `+page.server.ts` 149 + - parallel data loading for artist, tracks, albums 150 + - performance: ~1.66s → instant render 151 + 152 + **result**: 153 + - eliminated "flash of loading" across the app 154 + - improved SEO for artist and album pages 155 + - reduced code duplication (-308 lines, +256 lines net change) 156 + - consistent auth patterns everywhere 157 + 158 + ## performance impact 159 + 160 + ### before (client-side onMount pattern) 161 + 1. page renders with loading state 162 + 2. component mounts 163 + 3. fetch artist data (250ms) 164 + 4. fetch tracks data (1060ms) 165 + 5. fetch albums data (346ms) 166 + 6. total: ~1.66s before meaningful render 167 + 168 + ### after (server-side load pattern) 169 + 1. server fetches all data in parallel 170 + 2. page renders with complete data 171 + 3. total: instant render 172 + 173 + ### metrics 174 + - artist page load time: 1.66s → <200ms 175 + - eliminated sequential waterfall 176 + - reduced client-side API calls 177 + - better lighthouse scores 178 + 179 + ## browser caching considerations 180 + 181 + SvelteKit's load functions benefit from: 182 + - browser HTTP cache (respects Cache-Control headers) 183 + - SvelteKit's internal navigation cache 184 + - preloading on link hover 185 + 186 + but watch for: 187 + - stale data after mutations (invalidate with `invalidate()`) 188 + - localStorage caching (tracks cache uses this intentionally) 189 + - session token expiry (refresh in layout load) 190 + 191 + ## current issue (#225) 192 + 193 + there's an ongoing investigation into unwanted auto-play behavior after page refresh: 194 + - symptom: page refresh sometimes starts playing immediately 195 + - suspected cause: client-side caching of playback state 196 + - `autoplay_next` preference set to false but not respected 197 + - may be related to queue state restoration 198 + - needs investigation into what client-side state is persisting 199 + 200 + this highlights the importance of understanding the boundary between: 201 + - server-loaded data (authoritative, fresh on each load) 202 + - client state (persists in localStorage, may be stale) 203 + - when to use which pattern