feat: SSR shopping list by fetching initial data server-side
All checks were successful
CI / update (push) Successful in 3m33s

Server load now fetches the shopping list from the DB and passes it as
initialList. The sync layer seeds state immediately in the script block
(not onMount) so SSR renders the full list. SSE connects client-side
in onMount for real-time updates.
This commit is contained in:
2026-04-09 23:20:11 +02:00
parent 2dfed11fd6
commit 1472451ac4
3 changed files with 47 additions and 3 deletions

View File

@@ -166,6 +166,20 @@ export function createShoppingSync() {
}
}
/** Seed state from server-loaded data (safe to call during SSR). */
function seed(initialList: { version: number; items: ShoppingItem[] }, token?: string | null) {
if (token) shareToken = token;
version = initialList.version;
items = initialList.items || [];
status = 'synced';
}
/** Connect SSE for real-time updates (client-only, call in onMount). */
function connect(token?: string | null) {
if (token) shareToken = token;
connectSSE();
}
function addItem(name: string, user: string, category = 'Sonstiges') {
items = [...items, {
id: generateId(),
@@ -211,6 +225,8 @@ export function createShoppingSync() {
get version() { return version; },
apiUrl,
init,
seed,
connect,
addItem,
toggleItem,
removeItem,

View File

@@ -1,6 +1,8 @@
import type { PageServerLoad } from './$types';
import { redirect } from '@sveltejs/kit';
import { getShoppingUser } from '$lib/server/shoppingAuth';
import { dbConnect } from '$utils/db';
import { ShoppingList } from '$models/ShoppingList';
export const load: PageServerLoad = async ({ locals, url }) => {
const session = await locals.auth();
@@ -9,9 +11,24 @@ export const load: PageServerLoad = async ({ locals, url }) => {
// Allow access with valid share token even without session
if (!session && token) {
const user = await getShoppingUser(locals, url);
if (user) return { session: null, shareToken: token };
if (user) {
await dbConnect();
const list = await ShoppingList.findOne().lean();
return {
session: null,
shareToken: token,
initialList: list ? { version: list.version, items: list.items } : { version: 0, items: [] }
};
}
}
if (!session) throw redirect(302, '/login');
return { session, shareToken: null };
await dbConnect();
const list = await ShoppingList.findOne().lean();
return {
session,
shareToken: null,
initialList: list ? { version: list.version, items: list.items } : { version: 0, items: [] }
};
};

View File

@@ -20,6 +20,11 @@
let isGuest = $derived(!data.session);
const sync = getShoppingSync();
// Seed sync state immediately so SSR can render the list
if (data.initialList) {
sync.seed(data.initialList, data.shareToken);
}
const lang = $derived(detectCospendLang($page.url.pathname));
const loc = $derived(locale(lang));
@@ -141,7 +146,13 @@
let checkedCount = $derived(sync.items.filter(i => i.checked).length);
let totalCount = $derived(sync.items.length);
onMount(() => { sync.init(shareToken); });
onMount(() => {
if (data.initialList) {
sync.connect(shareToken);
} else {
sync.init(shareToken);
}
});
onDestroy(() => { sync.disconnect(); });
async function addItem() {