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.
This commit is contained in:
2026-05-08 16:01:59 +02:00
parent eb2ffac536
commit 0814803fc7
3 changed files with 61 additions and 29 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "homepage", "name": "homepage",
"version": "1.67.4", "version": "1.67.5",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+27 -11
View File
@@ -16,15 +16,8 @@ function addFavoriteStatus<T extends { _id: unknown }>(
})); }));
} }
export const load: PageLoad = async ({ params, fetch, parent }) => { async function loadFromIndexedDB() {
const parentData = await parent(); if (!await isOfflineDataAvailable()) return null;
const apiBase = `/api/${params.recipeLang}`;
const useOfflineData =
browser && (isOffline() || parentData.isOffline) && canUseOfflineData();
if (useOfflineData) {
try {
if (await isOfflineDataAvailable()) {
const [allBrief, seasonRecipes] = await Promise.all([ const [allBrief, seasonRecipes] = await Promise.all([
getAllBriefRecipes(), getAllBriefRecipes(),
getBriefRecipesInSeasonOn(new Date()) getBriefRecipesInSeasonOn(new Date())
@@ -33,9 +26,21 @@ export const load: PageLoad = async ({ params, fetch, parent }) => {
all_brief: rand_array(allBrief), all_brief: rand_array(allBrief),
season: rand_array(seasonRecipes), season: rand_array(seasonRecipes),
heroIndex: Math.random(), heroIndex: Math.random(),
isOffline: true isOffline: true as const
}; };
} }
export const load: PageLoad = async ({ params, fetch, parent }) => {
const parentData = await parent();
const apiBase = `/api/${params.recipeLang}`;
const canUseOffline = browser && canUseOfflineData();
const knownOffline = browser && (isOffline() || parentData.isOffline);
// Skip the network entirely when device is known offline.
if (canUseOffline && knownOffline) {
try {
const offline = await loadFromIndexedDB();
if (offline) return offline;
} catch (e) { } catch (e) {
console.error('Failed to load offline data:', e); console.error('Failed to load offline data:', e);
} }
@@ -54,7 +59,18 @@ export const load: PageLoad = async ({ params, fetch, parent }) => {
favorites = body.favorites ?? []; favorites = body.favorites ?? [];
} }
} catch { } 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); const marked = addFavoriteStatus(all_brief, favorites);
@@ -15,23 +15,28 @@ function addFavoriteStatus<T extends { _id: unknown }>(
})); }));
} }
export const load: PageLoad = async ({ params, fetch, parent }) => { async function loadFromIndexedDB(month: number) {
const parentData = await parent(); if (!await isOfflineDataAvailable()) return null;
const month = parseInt(params.month, 10);
const apiBase = `/api/${params.recipeLang}`;
const useOfflineData =
browser && (isOffline() || parentData.isOffline) && canUseOfflineData();
if (useOfflineData) {
try {
if (await isOfflineDataAvailable()) {
const recipes = await getBriefRecipesOverlappingMonth(month); const recipes = await getBriefRecipesOverlappingMonth(month);
return { return {
month, month,
season: rand_array(recipes), season: rand_array(recipes),
isOffline: true 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 canUseOffline = browser && canUseOfflineData();
const knownOffline = browser && (isOffline() || parentData.isOffline);
// Skip the network entirely when device is known offline.
if (canUseOffline && knownOffline) {
try {
const offline = await loadFromIndexedDB(month);
if (offline) return offline;
} catch (e) { } catch (e) {
console.error('Failed to load offline season data:', 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 ?? []; favorites = body.favorites ?? [];
} }
} catch { } 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 { return {