From 0814803fc7e6ea2b7b7039efe992bc046c6347eb Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Fri, 8 May 2026 16:01:59 +0200 Subject: [PATCH] fix(offline): IndexedDB fallback when API returns empty on /recipes & /season/[month] These two pages only fell back to IndexedDB when navigator.onLine was false. On mobile the device often reports online while the origin is flaky (502 / slow cellular / cached shell with stale connectivity), so the API call returned nothing and the pages rendered 0 recipes. Now both also fall back to IndexedDB when the API attempt yields an empty list, matching the pattern already used by [name]/+page.ts and icon/[icon]/+page.ts. --- package.json | 2 +- src/routes/[recipeLang=recipeLang]/+page.ts | 48 ++++++++++++------- .../season/[month]/+page.ts | 40 +++++++++++----- 3 files changed, 61 insertions(+), 29 deletions(-) diff --git a/package.json b/package.json index 07ff6b4c..1fa6579a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "homepage", - "version": "1.67.4", + "version": "1.67.5", "private": true, "type": "module", "scripts": { diff --git a/src/routes/[recipeLang=recipeLang]/+page.ts b/src/routes/[recipeLang=recipeLang]/+page.ts index 579171ef..8d9d814a 100644 --- a/src/routes/[recipeLang=recipeLang]/+page.ts +++ b/src/routes/[recipeLang=recipeLang]/+page.ts @@ -16,26 +16,31 @@ function addFavoriteStatus( })); } +async function loadFromIndexedDB() { + if (!await isOfflineDataAvailable()) return null; + const [allBrief, seasonRecipes] = await Promise.all([ + getAllBriefRecipes(), + getBriefRecipesInSeasonOn(new Date()) + ]); + return { + all_brief: rand_array(allBrief), + season: rand_array(seasonRecipes), + heroIndex: Math.random(), + isOffline: true as const + }; +} + export const load: PageLoad = async ({ params, fetch, parent }) => { const parentData = await parent(); const apiBase = `/api/${params.recipeLang}`; - const useOfflineData = - browser && (isOffline() || parentData.isOffline) && canUseOfflineData(); + const canUseOffline = browser && canUseOfflineData(); + const knownOffline = browser && (isOffline() || parentData.isOffline); - if (useOfflineData) { + // Skip the network entirely when device is known offline. + if (canUseOffline && knownOffline) { try { - if (await isOfflineDataAvailable()) { - const [allBrief, seasonRecipes] = await Promise.all([ - getAllBriefRecipes(), - getBriefRecipesInSeasonOn(new Date()) - ]); - return { - all_brief: rand_array(allBrief), - season: rand_array(seasonRecipes), - heroIndex: Math.random(), - isOffline: true - }; - } + const offline = await loadFromIndexedDB(); + if (offline) return offline; } catch (e) { console.error('Failed to load offline data:', e); } @@ -54,7 +59,18 @@ export const load: PageLoad = async ({ params, fetch, parent }) => { favorites = body.favorites ?? []; } } catch { - // Network unreachable — empty data; +page.svelte renders fallback layout. + // Network unreachable — IndexedDB fallback below picks up the slack. + } + + // API failed or returned empty (502, slow cellular, server hiccup) — fall + // back to IndexedDB so the cached PWA shell stays useful. + if (canUseOffline && !all_brief.length) { + try { + const offline = await loadFromIndexedDB(); + if (offline) return offline; + } catch (e) { + console.error('Failed to load offline data:', e); + } } const marked = addFavoriteStatus(all_brief, favorites); diff --git a/src/routes/[recipeLang=recipeLang]/season/[month]/+page.ts b/src/routes/[recipeLang=recipeLang]/season/[month]/+page.ts index 8705cb0e..4a343d92 100644 --- a/src/routes/[recipeLang=recipeLang]/season/[month]/+page.ts +++ b/src/routes/[recipeLang=recipeLang]/season/[month]/+page.ts @@ -15,23 +15,28 @@ function addFavoriteStatus( })); } +async function loadFromIndexedDB(month: number) { + if (!await isOfflineDataAvailable()) return null; + const recipes = await getBriefRecipesOverlappingMonth(month); + return { + month, + season: rand_array(recipes), + isOffline: true as const + }; +} + export const load: PageLoad = async ({ params, fetch, parent }) => { const parentData = await parent(); const month = parseInt(params.month, 10); const apiBase = `/api/${params.recipeLang}`; - const useOfflineData = - browser && (isOffline() || parentData.isOffline) && canUseOfflineData(); + const canUseOffline = browser && canUseOfflineData(); + const knownOffline = browser && (isOffline() || parentData.isOffline); - if (useOfflineData) { + // Skip the network entirely when device is known offline. + if (canUseOffline && knownOffline) { try { - if (await isOfflineDataAvailable()) { - const recipes = await getBriefRecipesOverlappingMonth(month); - return { - month, - season: rand_array(recipes), - isOffline: true - }; - } + const offline = await loadFromIndexedDB(month); + if (offline) return offline; } catch (e) { console.error('Failed to load offline season data:', e); } @@ -50,7 +55,18 @@ export const load: PageLoad = async ({ params, fetch, parent }) => { favorites = body.favorites ?? []; } } catch { - // Empty arrays — page will render with no recipes + // Network unreachable — IndexedDB fallback below picks up the slack. + } + + // API failed or returned empty (502, slow cellular, server hiccup) — fall + // back to IndexedDB so the cached PWA shell stays useful. + if (canUseOffline && !item_season.length) { + try { + const offline = await loadFromIndexedDB(month); + if (offline) return offline; + } catch (e) { + console.error('Failed to load offline season data:', e); + } } return {