-
+
+{#if showFallback && !canInstall && browser && !pwaInstallManager.isStandalone()}
+
+
+
+
Install InstaChef
+
{pwaInstallManager.getInstallInstructions()}
-
-
Install InstaRecipe
-
- {pwaInstallManager.getInstallInstructions()}
-
-
-
- {#if pwaInstallManager.getBrowserName() === 'safari'}
-
-
-
-
-
Use the Share button
-
- {:else}
-
-
-
-
-
Look for install button
-
- {/if}
-
-
-
-
-
-
+
✕
{/if}
\ No newline at end of file
diff --git a/src/routes/components/NotificationsScreen.svelte b/src/routes/components/NotificationsScreen.svelte
new file mode 100644
index 0000000..4b30eec
--- /dev/null
+++ b/src/routes/components/NotificationsScreen.svelte
@@ -0,0 +1,322 @@
+
+
+
+
+
+
+
+
+
Notifications
+
+
+
+
+
+
+
+ {#if enabled}
+
+ {:else}
+
+ {/if}
+
+
+
+
+ {enabled ? '● LIVE' : 'OFF'}
+
+
+ {enabled ? 'Push is on.' : "Get a ping when it's ready."}
+
+
+ {#if enabled}
+ We'll buzz you the moment a recipe is saved, fails, or needs a retry.
+ {:else}
+ Don't miss a plate. Allow notifications and we'll buzz you when a recipe is saved.
+ {/if}
+
+
+ {#if notifState.error}
+
{notifState.error}
+ {/if}
+
+ {#if notifState.permission === 'denied'}
+
+ Notifications are blocked. Enable them in your browser settings.
+
+ {:else}
+
+ {#if notifState.loading}
+ Working…
+ {:else if enabled}
+ Turn off
+ {:else}
+ Enable
+ {/if}
+
+ {/if}
+
+
+
+
+
+
+ {#each [
+ { color: 'var(--status-success)', title: 'Recipe saved', desc: 'Tap the notification to open it in Tandoor.' },
+ { color: 'var(--status-error)', title: 'Extraction failed', desc: 'Retry directly from the notification.' },
+ { color: 'var(--orange)', title: 'Long-running parses', desc: 'When a post is taking longer than usual.' }
+ ] as row, i}
+
0}>
+
+
+
{row.title}
+
{row.desc}
+
+
+ {/each}
+
+
+
+
+
Live queue
+
+
+ {sseConnected ? 'SSE connected' : 'SSE disconnected'}
+
+ {#if sseLastPing}
+ {sseLastPing}
+ {/if}
+
+
+
+
+
+
diff --git a/src/routes/components/PhaseTrack.svelte b/src/routes/components/PhaseTrack.svelte
new file mode 100644
index 0000000..a196818
--- /dev/null
+++ b/src/routes/components/PhaseTrack.svelte
@@ -0,0 +1,87 @@
+
+
+
+ {#each phases as p, i}
+ {@const state = i < current ? 'done' : i === current ? 'active' : 'idle'}
+
+ {#if i < phases.length - 1}
+
+ {/if}
+ {/each}
+
+
+
diff --git a/src/routes/components/RecipeSheet.svelte b/src/routes/components/RecipeSheet.svelte
new file mode 100644
index 0000000..f0845d0
--- /dev/null
+++ b/src/routes/components/RecipeSheet.svelte
@@ -0,0 +1,435 @@
+
+
+{#if item}
+
+
+
+
e.stopPropagation()}>
+
+
+
+
+
+
+
+
+
+
+ {statusLabel(item).toUpperCase()}
+
+
+
+
+
+
+
+ {recipe?.name || (isPending ? 'Waiting in line' : isCooking ? statusLabel(item) + '…' : 'Untitled')}
+
+ {#if recipe?.servings}
+
Serves {recipe.servings} · from {username(item.url)}
+ {/if}
+
+
+
+
+
+ {#if recipe?.keywords?.length > 0}
+
+ {#each recipe.keywords as kw}
+ #{kw}
+ {/each}
+
+ {/if}
+
+
+ {#if isCooking}
+
+
3-phase progress
+ {#each phases as p, i}
+ {@const ph = item.phases.find((x) => x.name === p.name) || { status: 'pending' }}
+ {@const done = ph.status === 'completed'}
+ {@const active = ph.status === 'in_progress'}
+
+
+
+ {#if done}
+
+
+
+ {/if}
+
+
+ {#if active}
+
RUNNING
+ {/if}
+
+ {/each}
+
+ {/if}
+
+
+ {#if isError && item.error}
+
+
+ !
+ Failed during {item.error.phase}
+
+
{item.error.message}
+
onRetry?.(item!.id)}>
+ Try again
+
+
+ {/if}
+
+
+
+
+
+ {#if isSuccess && (item.results?.tandoorUrl ?? item.tandoorRecipeId)}
+ {@const tandoorUrl =
+ item.results?.tandoorUrl ??
+ `/api/v1/recipe/${item.results?.tandoorRecipeId ?? item.tandoorRecipeId}/`}
+
+ Open in Tandoor
+
+ {/if}
+
+
+
+{/if}
+
+
diff --git a/src/routes/components/RecipeThumb.svelte b/src/routes/components/RecipeThumb.svelte
new file mode 100644
index 0000000..22ef42e
--- /dev/null
+++ b/src/routes/components/RecipeThumb.svelte
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+ {#if emoji}
+
{emoji}
+ {:else}
+
+
+
+ {/if}
+
+
+
+
diff --git a/src/routes/components/SectionHead.svelte b/src/routes/components/SectionHead.svelte
new file mode 100644
index 0000000..c294158
--- /dev/null
+++ b/src/routes/components/SectionHead.svelte
@@ -0,0 +1,31 @@
+
+
+
+ {#if emoji}{emoji} {/if}
+ {@render children()}
+
+
+
diff --git a/src/routes/components/TimelineRow.svelte b/src/routes/components/TimelineRow.svelte
new file mode 100644
index 0000000..f9d176f
--- /dev/null
+++ b/src/routes/components/TimelineRow.svelte
@@ -0,0 +1,193 @@
+
+
+
+
+
e.key === 'Enter' && onTap?.()}>
+
+
+ {#if isPending}
+
+ #{queuePosition ?? 1}
+
+ {:else}
+
+ {/if}
+ {#if isError}
+
!
+ {:else if isSuccess}
+
+
+
+ {/if}
+
+
+
+
+
+ {recipe?.name || (isPending ? 'Waiting in line…' : 'Untitled recipe')}
+
+
+ {username(item.url)}
+ ·
+ {relTime(item.createdAt)}
+
+ {#if isError && item.error}
+
{item.error.message?.slice(0, 60)}…
+ {/if}
+
+
+
+
+ {#if isError}
+ { e.stopPropagation(); onRetry?.(item.id); }}
+ aria-label="Retry"
+ >
+
+
+ {:else}
+
+ {/if}
+
+
+
+
diff --git a/src/routes/components/TopBar.svelte b/src/routes/components/TopBar.svelte
new file mode 100644
index 0000000..d0cc7af
--- /dev/null
+++ b/src/routes/components/TopBar.svelte
@@ -0,0 +1,106 @@
+
+
+
+
+
+
+
InstaChef
+
+
+ LIVE · {count} RECIPES
+
+
+
+
+
+
+ {#if notifCount > 0}
+
+ {/if}
+
+
+
+
+
diff --git a/src/routes/components/ic/Bell.svelte b/src/routes/components/ic/Bell.svelte
new file mode 100644
index 0000000..030d72b
--- /dev/null
+++ b/src/routes/components/ic/Bell.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/routes/components/ic/BellOff.svelte b/src/routes/components/ic/BellOff.svelte
new file mode 100644
index 0000000..f347f46
--- /dev/null
+++ b/src/routes/components/ic/BellOff.svelte
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
diff --git a/src/routes/components/ic/Check.svelte b/src/routes/components/ic/Check.svelte
new file mode 100644
index 0000000..255ec4f
--- /dev/null
+++ b/src/routes/components/ic/Check.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Chevron.svelte b/src/routes/components/ic/Chevron.svelte
new file mode 100644
index 0000000..1de18ca
--- /dev/null
+++ b/src/routes/components/ic/Chevron.svelte
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/src/routes/components/ic/Clipboard.svelte b/src/routes/components/ic/Clipboard.svelte
new file mode 100644
index 0000000..9d6bc16
--- /dev/null
+++ b/src/routes/components/ic/Clipboard.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/routes/components/ic/Close.svelte b/src/routes/components/ic/Close.svelte
new file mode 100644
index 0000000..b9a44cc
--- /dev/null
+++ b/src/routes/components/ic/Close.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Download.svelte b/src/routes/components/ic/Download.svelte
new file mode 100644
index 0000000..70d0a57
--- /dev/null
+++ b/src/routes/components/ic/Download.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/routes/components/ic/External.svelte b/src/routes/components/ic/External.svelte
new file mode 100644
index 0000000..d182166
--- /dev/null
+++ b/src/routes/components/ic/External.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Filter.svelte b/src/routes/components/ic/Filter.svelte
new file mode 100644
index 0000000..f7a8029
--- /dev/null
+++ b/src/routes/components/ic/Filter.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Link.svelte b/src/routes/components/ic/Link.svelte
new file mode 100644
index 0000000..7dcffa0
--- /dev/null
+++ b/src/routes/components/ic/Link.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/routes/components/ic/PhasePlating.svelte b/src/routes/components/ic/PhasePlating.svelte
new file mode 100644
index 0000000..69961ba
--- /dev/null
+++ b/src/routes/components/ic/PhasePlating.svelte
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/components/ic/PhasePrepping.svelte b/src/routes/components/ic/PhasePrepping.svelte
new file mode 100644
index 0000000..f329132
--- /dev/null
+++ b/src/routes/components/ic/PhasePrepping.svelte
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/routes/components/ic/PhaseSimmering.svelte b/src/routes/components/ic/PhaseSimmering.svelte
new file mode 100644
index 0000000..1cffac7
--- /dev/null
+++ b/src/routes/components/ic/PhaseSimmering.svelte
@@ -0,0 +1,20 @@
+
+
+ {#if animate}
+
+
+
+ {/if}
+
+
+
+ {#if animate}
+
+
+
+ {/if}
+
+
+
diff --git a/src/routes/components/ic/Plus.svelte b/src/routes/components/ic/Plus.svelte
new file mode 100644
index 0000000..aa0c0bb
--- /dev/null
+++ b/src/routes/components/ic/Plus.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Retry.svelte b/src/routes/components/ic/Retry.svelte
new file mode 100644
index 0000000..8b33ea8
--- /dev/null
+++ b/src/routes/components/ic/Retry.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Search.svelte b/src/routes/components/ic/Search.svelte
new file mode 100644
index 0000000..8477f92
--- /dev/null
+++ b/src/routes/components/ic/Search.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Settings.svelte b/src/routes/components/ic/Settings.svelte
new file mode 100644
index 0000000..0bdd3ea
--- /dev/null
+++ b/src/routes/components/ic/Settings.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/routes/components/ic/Share.svelte b/src/routes/components/ic/Share.svelte
new file mode 100644
index 0000000..4ca3544
--- /dev/null
+++ b/src/routes/components/ic/Share.svelte
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/src/routes/components/ic/Spark.svelte b/src/routes/components/ic/Spark.svelte
new file mode 100644
index 0000000..5282867
--- /dev/null
+++ b/src/routes/components/ic/Spark.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/components/ic/Trash.svelte b/src/routes/components/ic/Trash.svelte
new file mode 100644
index 0000000..790dbae
--- /dev/null
+++ b/src/routes/components/ic/Trash.svelte
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/src/routes/layout.css b/src/routes/layout.css
index d4b5078..ebe4b9e 100644
--- a/src/routes/layout.css
+++ b/src/routes/layout.css
@@ -1 +1,171 @@
-@import 'tailwindcss';
+/* ─── InstaChef design system ─────────────────────────────────────────────── */
+
+/* Brand + shared tokens (theme-independent) */
+:root {
+ --grad-1: #833AB4;
+ --grad-2: #C13584;
+ --grad-3: #E1306C;
+ --grad-4: #FD7E14;
+ --grad-5: #FCAF45;
+ --brand-gradient: linear-gradient(135deg, var(--grad-1) 0%, var(--grad-3) 45%, var(--grad-4) 75%, var(--grad-5) 100%);
+ --brand-gradient-soft: linear-gradient(135deg, #FCE9F3 0%, #FFEAD8 100%);
+
+ --purple: #833AB4;
+ --pink: #E1306C;
+ --orange: #FD7E14;
+ --yellow: #FCAF45;
+ --berry: #C13584;
+
+ --status-pending: #FCAF45;
+ --status-success: #2EA56A;
+ --status-error: #E64B4B;
+
+ --font-display: "Lilita One", "Caprasimo", system-ui, sans-serif;
+ --font-body: "DM Sans", -apple-system, system-ui, sans-serif;
+ --font-mono: "JetBrains Mono", ui-monospace, monospace;
+}
+
+/* Light theme */
+.ic-root[data-theme="light"] {
+ --bg: #FFF8F5;
+ --bg-tint: #FFEFE4;
+ --surface: #FFFFFF;
+ --surface-2: #FDF1EC;
+ --surface-3: #F7E5DC;
+ --ink: #1A0B1F;
+ --ink-2: #3A2A40;
+ --muted: #7A6B7D;
+ --muted-2: #A8989C;
+ --border: rgba(26, 11, 31, 0.08);
+ --border-strong: rgba(26, 11, 31, 0.14);
+ --shadow-sm: 0 1px 2px rgba(26, 11, 31, 0.04), 0 2px 8px rgba(26, 11, 31, 0.04);
+ --shadow-md: 0 4px 12px rgba(26, 11, 31, 0.06), 0 12px 32px rgba(26, 11, 31, 0.05);
+ --shadow-lg: 0 12px 28px rgba(193, 53, 132, 0.18), 0 24px 60px rgba(131, 58, 180, 0.15);
+}
+
+/* Dark theme */
+.ic-root[data-theme="dark"] {
+ --bg: #110510;
+ --bg-tint: #1A0A1F;
+ --surface: #1F0F24;
+ --surface-2: #2A1730;
+ --surface-3: #371E3E;
+ --ink: #FCEFE5;
+ --ink-2: #E0D2DA;
+ --muted: #A38FA8;
+ --muted-2: #6E5A73;
+ --border: rgba(255, 235, 245, 0.08);
+ --border-strong: rgba(255, 235, 245, 0.16);
+ --shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4), 0 2px 8px rgba(0, 0, 0, 0.3);
+ --shadow-md: 0 4px 16px rgba(0, 0, 0, 0.4), 0 12px 32px rgba(0, 0, 0, 0.3);
+ --shadow-lg: 0 12px 28px rgba(225, 48, 108, 0.35), 0 24px 60px rgba(131, 58, 180, 0.3);
+}
+
+/* ─── Base reset ──────────────────────────────────────────────────────────── */
+*, *::before, *::after { box-sizing: border-box; }
+
+html, body {
+ margin: 0;
+ padding: 0;
+ height: 100%;
+}
+
+.ic-root {
+ font-family: var(--font-body);
+ color: var(--ink);
+ background: var(--bg);
+ -webkit-font-smoothing: antialiased;
+ min-height: 100dvh;
+}
+
+/* ─── Utility classes ─────────────────────────────────────────────────────── */
+.ic-display {
+ font-family: var(--font-display);
+ font-weight: 400;
+ letter-spacing: -0.005em;
+ line-height: 1;
+}
+
+.ic-grad-text {
+ background: var(--brand-gradient);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+.ic-scroll {
+ scrollbar-width: none;
+ -ms-overflow-style: none;
+}
+.ic-scroll::-webkit-scrollbar { display: none; }
+
+button.ic-btn {
+ background: none;
+ border: 0;
+ padding: 0;
+ cursor: pointer;
+ font: inherit;
+ color: inherit;
+ -webkit-tap-highlight-color: transparent;
+}
+
+/* ─── Animations ──────────────────────────────────────────────────────────── */
+@keyframes ic-steam {
+ 0% { transform: translateY(0) translateX(0) scale(0.6); opacity: 0; }
+ 25% { opacity: 0.85; }
+ 100% { transform: translateY(-44px) translateX(var(--drift, 4px)) scale(1.4); opacity: 0; }
+}
+.ic-steam {
+ animation: ic-steam 2.4s ease-out infinite;
+ animation-delay: var(--delay, 0s);
+}
+
+@keyframes ic-bubble {
+ 0%, 100% { transform: translateY(0) scale(1); }
+ 50% { transform: translateY(-2px) scale(1.05); }
+}
+.ic-bubble { animation: ic-bubble 1.6s ease-in-out infinite; }
+
+@keyframes ic-chop {
+ 0%, 100% { transform: rotate(-30deg) translateY(0); }
+ 50% { transform: rotate(-8deg) translateY(-2px); }
+}
+.ic-chop { animation: ic-chop 0.9s cubic-bezier(.7,0,.3,1) infinite; transform-origin: bottom right; }
+
+@keyframes ic-shimmer {
+ 0% { transform: translateX(-100%); }
+ 100% { transform: translateX(200%); }
+}
+.ic-shimmer { animation: ic-shimmer 2s ease-in-out infinite; }
+
+@keyframes ic-pulse {
+ 0%, 100% { opacity: 1; transform: scale(1); }
+ 50% { opacity: 0.55; transform: scale(0.95); }
+}
+.ic-pulse { animation: ic-pulse 1.4s ease-in-out infinite; }
+
+@keyframes ic-slide-up {
+ from { transform: translateY(100%); }
+ to { transform: translateY(0); }
+}
+.ic-slide-up { animation: ic-slide-up 0.32s cubic-bezier(.2,.7,.2,1); }
+
+@keyframes ic-fade {
+ from { opacity: 0; }
+ to { opacity: 1; }
+}
+.ic-fade { animation: ic-fade 0.24s ease-out; }
+
+@keyframes ic-pop {
+ 0% { transform: scale(0.6); opacity: 0; }
+ 60% { transform: scale(1.08); opacity: 1; }
+ 100% { transform: scale(1); }
+}
+.ic-pop { animation: ic-pop 0.4s cubic-bezier(.2,1.4,.4,1); }
+
+@keyframes ic-live {
+ 0%, 100% { opacity: 1; }
+ 50% { opacity: 0.3; }
+}
+.ic-live { animation: ic-live 1.4s ease-in-out infinite; }
+
diff --git a/src/routes/share/+page.svelte b/src/routes/share/+page.svelte
index 0136613..b4d18f9 100644
--- a/src/routes/share/+page.svelte
+++ b/src/routes/share/+page.svelte
@@ -2,13 +2,11 @@
import { page } from '$app/stores';
import { goto } from '$app/navigation';
import { onMount } from 'svelte';
- import UrlInputSection from './components/UrlInputSection.svelte';
+ import AddUrlScreen from '../components/AddUrlScreen.svelte';
- let status = $state('idle');
- let logs = $state
([]);
+ let status = $state<'idle' | 'enqueuing' | 'success' | 'error'>('idle');
// URL param parsing for Share Target
- // Instagram typically shares text that contains the URL, so we might need to parse it out
let sharedText = $derived($page.url.searchParams.get('text') || '');
let sharedUrl = $derived($page.url.searchParams.get('url') || '');
@@ -17,32 +15,27 @@
return match ? match[0] : null;
}
- let targetUrl = $derived(sharedUrl || extractUrl(sharedText));
+ let targetUrl = $derived(sharedUrl || extractUrl(sharedText) || '');
// Track if we've already auto-processed to prevent duplicate processing
let hasAutoProcessed = $state(false);
// Auto-process URL if provided via share target
- // Use onMount instead of $effect for side effects (SvelteKit best practice)
onMount(() => {
if (targetUrl && status === 'idle' && !hasAutoProcessed) {
hasAutoProcessed = true;
- process();
+ process(targetUrl);
}
});
- async function process(url?: string) {
- const urlToProcess = url || targetUrl;
- if (!urlToProcess) return;
-
+ async function process(url: string) {
+ if (!url) return;
status = 'enqueuing';
- logs = [...logs, '🚀 Enqueuing extraction from: ' + urlToProcess];
try {
- // Enqueue URL for background processing
const response = await fetch('/api/queue', {
method: 'POST',
- body: JSON.stringify({ url: urlToProcess }),
+ body: JSON.stringify({ url }),
headers: { 'Content-Type': 'application/json' }
});
@@ -52,88 +45,100 @@
}
const queueItem = await response.json();
- logs = [...logs, `✅ URL enqueued successfully with ID: ${queueItem.id}`];
- logs = [...logs, '🔄 Redirecting to queue dashboard...'];
+ status = 'success';
- // Small delay to show the success message
setTimeout(() => {
- // Redirect to homepage (queue dashboard) with the queue item ID highlighted
goto(`/?highlight=${queueItem.id}`);
- }, 1500);
-
+ }, 800);
} catch (e) {
status = 'error';
- const errorMessage = e instanceof Error ? e.message : 'Unknown error';
- logs = [...logs, `❌ Error: ${errorMessage}`];
+ console.error('Failed to enqueue:', e);
}
}
-
- function retry() {
- status = 'idle';
- logs = [...logs, 'Retrying...'];
- process();
- }
- Share to InstaRecipe
-
+ Add Recipe — InstaChef
+
-
-
-
Share to InstaRecipe
-
- {#if targetUrl}
- Processing your shared recipe...
+{#if status === 'enqueuing' || status === 'success'}
+
+
+
+ {#if status === 'enqueuing'}
+
+
Adding to queue…
{:else}
- Paste an Instagram recipe URL to extract it
+
✓
+
Added! Redirecting…
{/if}
-
+
{targetUrl}
+
+{:else}
+
goto('/')}
+ onSubmit={process}
+ />
+{/if}
- {#if !targetUrl}
-
- {:else}
-
-
-
-
Processing URL:
-
{targetUrl}
-
- {#if status === 'enqueuing'}
-
-
-
Enqueuing for processing...
-
- {:else if status === 'error'}
-
- ❌ Error occurred
-
-
- Retry
-
- {:else}
-
✅ Ready to process
- {/if}
-
-
- {/if}
-
-
- {#if logs.length > 0}
-
-
-
Process Log:
-
- {#each logs as log}
-
{log}
- {/each}
-
-
-
- {/if}
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/static/icon-256.png b/static/icon-256.png
new file mode 100644
index 0000000..7f30850
Binary files /dev/null and b/static/icon-256.png differ
diff --git a/static/manifest.json b/static/manifest.json
index c830e97..cb682a7 100644
--- a/static/manifest.json
+++ b/static/manifest.json
@@ -4,8 +4,8 @@
"start_url": "/",
"scope": "/",
"display": "standalone",
- "theme_color": "#ffffff",
- "background_color": "#ffffff",
+ "theme_color": "#FFF8F5",
+ "background_color": "#FFF8F5",
"icons": [
{
"src": "/favicon.png",