my website at ewancroft.uk
6
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: implement fetch function injection for improved data fetching in AT Protocol services

+205 -92
+10
src/hooks.server.ts
··· 1 + import type { Handle } from '@sveltejs/kit'; 2 + 3 + export const handle: Handle = async ({ event, resolve }) => { 4 + const response = await resolve(event, { 5 + filterSerializedResponseHeaders: (name) => { 6 + return name === 'content-type' || name.startsWith('x-'); 7 + } 8 + }); 9 + return response; 10 + };
+5 -17
src/lib/components/layout/Footer.svelte
··· 1 1 <script lang="ts"> 2 - import { onMount } from 'svelte'; 3 - import { fetchProfile, type ProfileData, fetchSiteInfo, type SiteInfoData } from '$lib/services/atproto'; 2 + import type { ProfileData, SiteInfoData } from '$lib/services/atproto'; 4 3 5 - let profile: ProfileData | null = null; 6 - let siteInfo: SiteInfoData | null = null; 7 - let loading = true; 4 + export let profile: ProfileData | null = null; 5 + export let siteInfo: SiteInfoData | null = null; 6 + let loading = false; 8 7 let error: string | null = null; 9 8 let copyrightText: string; 10 9 ··· 32 31 } 33 32 } 34 33 35 - onMount(async () => { 36 - try { 37 - [profile, siteInfo] = await Promise.all([ 38 - fetchProfile(), 39 - fetchSiteInfo() 40 - ]); 41 - } catch (err) { 42 - error = err instanceof Error ? err.message : 'Failed to load data'; 43 - } finally { 44 - loading = false; 45 - } 46 - }); 34 + // Data is provided by layout load; no client-side fetch here to avoid using window.fetch during navigation. 47 35 </script> 48 36 49 37 <footer
+72 -19
src/lib/services/atproto/agents.ts
··· 1 1 import { AtpAgent } from '@atproto/api'; 2 2 import type { ResolvedIdentity } from './types'; 3 3 4 + /** 5 + * Creates an AtpAgent with optional fetch function injection 6 + */ 7 + export function createAgent(service: string, fetchFn?: typeof fetch): AtpAgent { 8 + // If we have an injected fetch, wrap it to ensure we handle headers correctly 9 + const wrappedFetch = fetchFn ? async (url: URL | RequestInfo, init?: RequestInit) => { 10 + // Convert URL to string if needed 11 + const urlStr = url instanceof URL ? url.toString() : url; 12 + 13 + // Make the request with the injected fetch 14 + const response = await fetchFn(urlStr, init); 15 + 16 + // Create a new response with the same body but add content-type if missing 17 + const headers = new Headers(response.headers); 18 + if (!headers.has('content-type')) { 19 + headers.set('content-type', 'application/json'); 20 + } 21 + 22 + return new Response(response.body, { 23 + status: response.status, 24 + statusText: response.statusText, 25 + headers 26 + }); 27 + } : undefined; 28 + 29 + return new AtpAgent({ 30 + service, 31 + ...(wrappedFetch && { fetch: wrappedFetch }) 32 + }); 33 + } 34 + 4 35 // Primary Microcosm Constellation endpoint 5 - export const constellationAgent = new AtpAgent({ service: 'https://constellation.microcosm.blue' }); 36 + export const constellationAgent = createAgent('https://constellation.microcosm.blue'); 6 37 7 38 // Default fallback agent for public Bluesky API calls 8 - export const defaultAgent = new AtpAgent({ service: 'https://public.api.bsky.app' }); 39 + export const defaultAgent = createAgent('https://public.api.bsky.app'); 9 40 10 41 // Cached agents 11 42 let resolvedAgent: AtpAgent | null = null; ··· 14 45 /** 15 46 * Resolves a DID to find its PDS endpoint using Slingshot. 16 47 */ 17 - export async function resolveIdentity(did: string): Promise<ResolvedIdentity> { 48 + export async function resolveIdentity( 49 + did: string, 50 + fetchFn?: typeof fetch 51 + ): Promise<ResolvedIdentity> { 18 52 console.info(`[Identity] Resolving DID: ${did}`); 19 - 20 - const response = await fetch( 21 - `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent(did)}` 53 + 54 + // Prefer an injected fetch (from SvelteKit load), fall back to global fetch 55 + const _fetch = fetchFn ?? globalThis.fetch; 56 + 57 + const response = await _fetch( 58 + `https://slingshot.microcosm.blue/xrpc/com.bad-example.identity.resolveMiniDoc?identifier=${encodeURIComponent( 59 + did 60 + )}` 22 61 ); 23 62 24 63 if (!response.ok) { ··· 26 65 throw new Error(`Failed to resolve identifier via Slingshot: ${response.status} ${response.statusText}`); 27 66 } 28 67 29 - console.debug(`[Identity] Raw response:`, await response.clone().text()); 30 - const data = await response.json(); 68 + // Some fetch implementations in Node (undici wrappers) can throw when calling Response.clone(). 69 + // Read the text once and parse it instead of cloning to avoid private field access errors. 70 + const rawText = await response.text(); 71 + console.debug(`[Identity] Raw response:`, rawText); 72 + let data: any; 73 + try { 74 + data = JSON.parse(rawText); 75 + } catch (err) { 76 + console.error('[Identity] Failed to parse identity resolver response as JSON', err); 77 + throw err; 78 + } 31 79 32 80 if (!data.did || !data.pds) { 33 81 throw new Error('Invalid response from identity resolver'); ··· 39 87 /** 40 88 * Gets or creates an agent for the public Bluesky API with PDS fallback 41 89 */ 42 - export async function getPublicAgent(did: string): Promise<AtpAgent> { 90 + export async function getPublicAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> { 43 91 console.info(`[Agent] Getting public agent for DID: ${did}`); 44 92 if (resolvedAgent) { 45 93 console.debug('[Agent] Using cached agent'); ··· 60 108 console.warn('[Agent] Constellation endpoint unreachable:', constellationErr); 61 109 } 62 110 63 - // Then try Slingshot for PDS resolution 64 - console.info('[Agent] Attempting Slingshot resolution'); 65 - const resolved = await resolveIdentity(did); 111 + // Then try Slingshot for PDS resolution 112 + console.info('[Agent] Attempting Slingshot resolution'); 113 + const resolved = await resolveIdentity(did, fetchFn); 66 114 console.info(`[Agent] Resolved PDS endpoint: ${resolved.pds}`); 67 - resolvedAgent = new AtpAgent({ service: resolved.pds }); 115 + resolvedAgent = createAgent(resolved.pds, fetchFn); 68 116 return resolvedAgent; 69 117 } catch (err) { 70 118 console.error('[Agent] All Microcosm endpoints failed, falling back to Bluesky:', err); ··· 74 122 }/** 75 123 * Gets or creates a PDS-specific agent 76 124 */ 77 - export async function getPDSAgent(did: string): Promise<AtpAgent> { 125 + export async function getPDSAgent(did: string, fetchFn?: typeof fetch): Promise<AtpAgent> { 78 126 if (pdsAgent) return pdsAgent; 79 127 80 128 try { 81 - const resolved = await resolveIdentity(did); 82 - pdsAgent = new AtpAgent({ service: resolved.pds }); 129 + const resolved = await resolveIdentity(did, fetchFn); 130 + pdsAgent = createAgent(resolved.pds, fetchFn); 83 131 return pdsAgent; 84 132 } catch (err) { 85 133 console.error('Failed to resolve PDS for DID:', err); ··· 96 144 export async function withFallback<T>( 97 145 did: string, 98 146 operation: (agent: AtpAgent) => Promise<T>, 99 - usePDSFirst = false 147 + usePDSFirst = false, 148 + fetchFn?: typeof fetch 100 149 ): Promise<T> { 150 + const defaultAgentFn = () => fetchFn 151 + ? createAgent('https://public.api.bsky.app', fetchFn) 152 + : Promise.resolve(defaultAgent); 153 + 101 154 const agents = usePDSFirst 102 - ? [() => getPDSAgent(did), () => Promise.resolve(defaultAgent)] 103 - : [() => Promise.resolve(defaultAgent), () => getPDSAgent(did)]; 155 + ? [() => getPDSAgent(did, fetchFn), defaultAgentFn] 156 + : [defaultAgentFn, () => getPDSAgent(did, fetchFn)]; 104 157 105 158 let lastError: any; 106 159
+40 -20
src/lib/services/atproto/fetch.ts
··· 6 6 /** 7 7 * Fetches user profile from AT Protocol 8 8 */ 9 - export async function fetchProfile(): Promise<ProfileData> { 9 + export async function fetchProfile(fetchFn?: typeof fetch): Promise<ProfileData> { 10 10 console.info('[Profile] Fetching profile data'); 11 11 const cacheKey = `profile:${PUBLIC_ATPROTO_DID}`; 12 12 const cached = cache.get<ProfileData>(cacheKey); ··· 18 18 try { 19 19 console.info('[Profile] Cache miss, fetching from network'); 20 20 // Profile data is public, try Bluesky API first, then PDS 21 - const profile = await withFallback(PUBLIC_ATPROTO_DID, async (agent) => { 22 - console.debug('[Profile] Attempting profile fetch with agent:', agent.service.toString()); 23 - const response = await agent.getProfile({ actor: PUBLIC_ATPROTO_DID }); 24 - return response.data; 25 - }); 21 + const profile = await withFallback( 22 + PUBLIC_ATPROTO_DID, 23 + async (agent) => { 24 + console.debug('[Profile] Attempting profile fetch with agent:', agent.service.toString()); 25 + const response = await agent.getProfile({ actor: PUBLIC_ATPROTO_DID }); 26 + return response.data; 27 + }, 28 + false, 29 + fetchFn 30 + ); 26 31 27 32 const data: ProfileData = { 28 33 did: profile.did, ··· 49 54 /** 50 55 * Fetches user status from custom lexicon 51 56 */ 52 - export async function fetchStatus(): Promise<StatusData | null> { 57 + export async function fetchStatus(fetchFn?: typeof fetch): Promise<StatusData | null> { 53 58 console.info('[Status] Fetching status data'); 54 59 const cacheKey = `status:${PUBLIC_ATPROTO_DID}`; 55 60 const cached = cache.get<StatusData>(cacheKey); ··· 71 76 }); 72 77 return response.data.records; 73 78 }, 74 - true 79 + true, 80 + fetchFn 75 81 ); // usePDSFirst = true 76 82 77 83 if (records.length === 0) return null; ··· 93 99 /** 94 100 * Fetches site information from custom lexicon 95 101 */ 96 - export async function fetchSiteInfo(): Promise<SiteInfoData | null> { 102 + export async function fetchSiteInfo(fetchFn?: typeof fetch): Promise<SiteInfoData | null> { 97 103 const cacheKey = `siteinfo:${PUBLIC_ATPROTO_DID}`; 98 104 const cached = cache.get<SiteInfoData>(cacheKey); 99 105 if (cached) return cached; 100 106 101 107 try { 102 108 // Custom collection, prefer PDS first 103 - const value = await withFallback( 109 + const result = await withFallback( 104 110 PUBLIC_ATPROTO_DID, 105 111 async (agent) => { 106 - const response = await agent.com.atproto.repo.getRecord({ 107 - repo: PUBLIC_ATPROTO_DID, 108 - collection: 'uk.ewancroft.site.info', 109 - rkey: 'self' 110 - }); 111 - return response.data.value; 112 + try { 113 + const response = await agent.com.atproto.repo.getRecord({ 114 + repo: PUBLIC_ATPROTO_DID, 115 + collection: 'uk.ewancroft.site.info', 116 + rkey: 'self' 117 + }); 118 + return response.data; 119 + } catch (err: any) { 120 + // If record not found, return null instead of throwing 121 + if (err.error === 'RecordNotFound') { 122 + return null; 123 + } 124 + throw err; 125 + } 112 126 }, 113 - true 127 + true, 128 + fetchFn 114 129 ); // usePDSFirst = true 115 130 116 - const data = value as SiteInfoData; 131 + if (!result || !result.value) { 132 + return null; 133 + } 134 + 135 + const data = result.value as SiteInfoData; 117 136 cache.set(cacheKey, data); 118 137 return data; 119 138 } catch (error) { ··· 125 144 /** 126 145 * Fetches links from Linkat board 127 146 */ 128 - export async function fetchLinks(): Promise<LinkData | null> { 147 + export async function fetchLinks(fetchFn?: typeof fetch): Promise<LinkData | null> { 129 148 const cacheKey = `links:${PUBLIC_ATPROTO_DID}`; 130 149 const cached = cache.get<LinkData>(cacheKey); 131 150 if (cached) return cached; ··· 142 161 }); 143 162 return response.data.value; 144 163 }, 145 - true 164 + true, 165 + fetchFn 146 166 ); // usePDSFirst = true 147 167 148 168 // Validate the response has the expected structure
+50 -29
src/lib/services/atproto/posts.ts
··· 1 1 import { PUBLIC_ATPROTO_DID } from '$env/static/public'; 2 2 import { cache } from './cache'; 3 - import { withFallback, defaultAgent } from './agents'; 3 + import { withFallback, defaultAgent, createAgent } from './agents'; 4 4 import { resolveIdentity } from './agents'; 5 5 import { buildPdsBlobUrl } from './media'; 6 6 import { fetchAllEngagement } from './engagement'; ··· 17 17 /** 18 18 * Fetches all Leaflet publications for a user 19 19 */ 20 - export async function fetchLeafletPublications(): Promise<LeafletPublicationsData> { 20 + export async function fetchLeafletPublications(fetchFn?: typeof fetch): Promise<LeafletPublicationsData> { 21 21 console.info('[Leaflet] Fetching publications'); 22 22 const cacheKey = `leaflet:publications:${PUBLIC_ATPROTO_DID}`; 23 23 const cached = cache.get<LeafletPublicationsData>(cacheKey); ··· 41 41 }); 42 42 return response.data.records; 43 43 }, 44 - true 44 + true, 45 + fetchFn 45 46 ); 46 47 47 48 for (const pubRecord of publicationsRecords) { ··· 54 55 uri: pubRecord.uri, 55 56 basePath: pubValue.base_path, 56 57 description: pubValue.description, 57 - icon: pubValue.icon ? await getBlobUrl(pubValue.icon) : undefined 58 + icon: pubValue.icon ? await getBlobUrl(pubValue.icon, fetchFn) : undefined 58 59 }); 59 60 } 60 61 ··· 70 71 /** 71 72 * Helper function to get a blob URL for Leaflet publication icons 72 73 */ 73 - async function getBlobUrl(blob: any): Promise<string | undefined> { 74 + async function getBlobUrl(blob: any, fetchFn?: typeof fetch): Promise<string | undefined> { 74 75 try { 75 76 const cid = blob.ref?.$link || blob.cid; 76 77 if (!cid) return undefined; 77 78 78 - const resolved = await resolveIdentity(PUBLIC_ATPROTO_DID); 79 + const resolved = await resolveIdentity(PUBLIC_ATPROTO_DID, fetchFn); 79 80 return buildPdsBlobUrl(resolved.pds, PUBLIC_ATPROTO_DID, cid); 80 81 } catch (error) { 81 82 console.warn('Failed to resolve blob URL:', error); ··· 87 88 * Fetches blog posts from both WhiteWind and Leaflet sources 88 89 * Now supports multiple Leaflet publications 89 90 */ 90 - export async function fetchBlogPosts(): Promise<BlogPostsData> { 91 + export async function fetchBlogPosts(fetchFn?: typeof fetch): Promise<BlogPostsData> { 91 92 const cacheKey = `blogposts:${PUBLIC_ATPROTO_DID}`; 92 93 const cached = cache.get<BlogPostsData>(cacheKey); 93 94 if (cached) return cached; ··· 106 107 }); 107 108 return response.data.records; 108 109 }, 109 - true 110 + true, 111 + fetchFn 110 112 ); 111 113 112 114 for (const record of whiteWindRecords) { ··· 132 134 // Fetch Leaflet publications and documents 133 135 try { 134 136 // Get all publications first 135 - const publicationsData = await fetchLeafletPublications(); 137 + const publicationsData = await fetchLeafletPublications(fetchFn); 136 138 const publicationsMap = new Map<string, LeafletPublication>(); 137 139 for (const pub of publicationsData.publications) { 138 140 publicationsMap.set(pub.uri, pub); ··· 149 151 }); 150 152 return response.data.records; 151 153 }, 152 - true 154 + true, 155 + fetchFn 153 156 ); 154 157 155 158 for (const record of leafletDocsRecords) { ··· 201 204 /** 202 205 * Fetches the latest Bluesky post (including replies and reposts) 203 206 */ 204 - export async function fetchLatestBlueskyPost(): Promise<BlueskyPost | null> { 207 + export async function fetchLatestBlueskyPost(fetchFn?: typeof fetch): Promise<BlueskyPost | null> { 205 208 console.log('[fetchLatestBlueskyPost] Starting fetch...'); 206 209 const cacheKey = `blueskypost:latest:${PUBLIC_ATPROTO_DID}`; 207 210 const cached = cache.get<BlueskyPost>(cacheKey); ··· 212 215 213 216 try { 214 217 console.log('[fetchLatestBlueskyPost] Fetching author feed...'); 215 - // Use getAuthorFeed to get posts AND reposts in chronological order 216 - const feedResponse = await defaultAgent.getAuthorFeed({ 217 - actor: PUBLIC_ATPROTO_DID, 218 - limit: 5 219 - }); 218 + // Use withFallback to get posts AND reposts in chronological order 219 + const feedResponse = await withFallback( 220 + PUBLIC_ATPROTO_DID, 221 + async (agent) => { 222 + return agent.getAuthorFeed({ 223 + actor: PUBLIC_ATPROTO_DID, 224 + limit: 5 225 + }); 226 + }, 227 + false, 228 + fetchFn 229 + ); 220 230 221 231 const feed = feedResponse.data.feed; 222 232 console.log('[fetchLatestBlueskyPost] Feed items fetched:', feed.length); ··· 249 259 } 250 260 251 261 // Fetch the full post data 252 - const post = await fetchPostFromUri(latestPostData.uri, 0); 262 + const post = await fetchPostFromUri(latestPostData.uri, 0, fetchFn); 253 263 254 264 if (!post) { 255 265 console.warn('[fetchLatestBlueskyPost] fetchPostFromUri returned null'); ··· 277 287 /** 278 288 * Recursively fetches a Bluesky post by URI, supporting quoted posts up to 2 levels deep 279 289 */ 280 - export async function fetchPostFromUri(uri: string, depth: number): Promise<BlueskyPost | null> { 290 + export async function fetchPostFromUri( 291 + uri: string, 292 + depth: number, 293 + fetchFn?: typeof fetch 294 + ): Promise<BlueskyPost | null> { 281 295 console.log(`[fetchPostFromUri] Starting fetch at depth ${depth} for URI:`, uri); 282 296 283 297 if (depth >= 3) { ··· 287 301 288 302 try { 289 303 console.log(`[fetchPostFromUri] Fetching post thread from Bluesky API...`); 290 - const threadResponse = await defaultAgent.getPostThread({ uri, depth: 0 }); 304 + const threadResponse = await withFallback( 305 + PUBLIC_ATPROTO_DID, 306 + async (agent) => { 307 + return agent.getPostThread({ uri, depth: 0 }); 308 + }, 309 + false, 310 + fetchFn 311 + ); 291 312 292 313 if (!threadResponse.data.thread || !('post' in threadResponse.data.thread)) { 293 314 console.warn(`[fetchPostFromUri] No valid thread data found for URI:`, uri); ··· 434 455 const quotedRecord = embed.record?.record || embed.record; 435 456 console.log(`[fetchPostFromUri] Quoted record in recordWithMedia:`, quotedRecord?.uri); 436 457 if (quotedRecord && typeof quotedRecord.uri === 'string') { 437 - quotedPostUri = quotedRecord.uri; 438 - console.log( 439 - `[fetchPostFromUri] Recursively fetching quoted post at depth ${depth + 1}:`, 440 - quotedPostUri 441 - ); 442 - if (quotedPostUri) { 443 - quotedPost = (await fetchPostFromUri(quotedPostUri, depth + 1)) ?? undefined; 458 + quotedPostUri = quotedRecord.uri; 459 + console.log( 460 + `[fetchPostFromUri] Recursively fetching quoted post at depth ${depth + 1}:`, 461 + quotedPostUri 462 + ); 463 + if (quotedPostUri) { 464 + quotedPost = (await fetchPostFromUri(quotedPostUri, depth + 1, fetchFn)) ?? undefined; 444 465 console.log(`[fetchPostFromUri] Quoted post fetched:`, quotedPost ? 'success' : 'failed'); 445 466 } 446 467 } ··· 458 479 quotedPostUri 459 480 ); 460 481 if (quotedPostUri) { 461 - quotedPost = (await fetchPostFromUri(quotedPostUri, depth + 1)) ?? undefined; 482 + quotedPost = (await fetchPostFromUri(quotedPostUri, depth + 1, fetchFn)) ?? undefined; 462 483 console.log(`[fetchPostFromUri] Quoted post fetched:`, quotedPost ? 'success' : 'failed'); 463 484 } 464 485 } ··· 470 491 if (value.reply) { 471 492 console.log(`[fetchPostFromUri] Post is a reply, fetching parent...`); 472 493 if (value.reply.parent?.uri) { 473 - replyParent = (await fetchPostFromUri(value.reply.parent.uri, depth + 1)) ?? undefined; 494 + replyParent = (await fetchPostFromUri(value.reply.parent.uri, depth + 1, fetchFn)) ?? undefined; 474 495 } 475 496 if (value.reply.root?.uri && value.reply.root.uri !== value.reply.parent?.uri) { 476 - replyRoot = (await fetchPostFromUri(value.reply.root.uri, depth + 1)) ?? undefined; 497 + replyRoot = (await fetchPostFromUri(value.reply.root.uri, depth + 1, fetchFn)) ?? undefined; 477 498 } 478 499 } 479 500
+5 -2
src/routes/+layout.svelte
··· 3 3 import { Header, Footer, ScrollToTop } from '$lib/components/layout'; 4 4 import { MetaTags } from '$lib/components/seo'; 5 5 import { createSiteMeta, type SiteMetadata } from '$lib/helper/siteMeta'; 6 + import type { ProfileData, SiteInfoData } from '$lib/services/atproto'; 6 7 import type { Snippet } from 'svelte'; 7 8 import { onMount } from 'svelte'; 8 9 ··· 10 11 data: { 11 12 siteMeta: SiteMetadata; 12 13 meta?: Partial<SiteMetadata>; 14 + profile?: ProfileData | null; 15 + siteInfo?: SiteInfoData | null; 13 16 }; 14 17 children: Snippet; 15 18 } ··· 75 78 76 79 <div class="flex min-h-screen flex-col overflow-x-hidden bg-canvas-50 text-ink-900 dark:bg-canvas-950 dark:text-ink-50"> 77 80 <Header /> 78 - 81 + 79 82 <main class="container mx-auto flex-grow px-4 py-8"> 80 83 <ScrollToTop /> 81 84 {@render children()} 82 85 </main> 83 86 84 - <Footer /> 87 + <Footer profile={data.profile} siteInfo={data.siteInfo} /> 85 88 </div>
+20 -2
src/routes/+layout.ts
··· 1 1 import type { LayoutLoad } from './$types'; 2 2 import { createSiteMeta, type SiteMetadata, defaultSiteMeta } from '$lib/helper/siteMeta'; 3 + import { fetchProfile, fetchSiteInfo } from '$lib/services/atproto'; 3 4 4 - export const load: LayoutLoad = async ({ url }) => { 5 + export const load: LayoutLoad = async ({ url, fetch }) => { 5 6 // Provide the default site metadata 6 7 const siteMeta: SiteMetadata = createSiteMeta({ 7 8 title: defaultSiteMeta.title, ··· 9 10 url: url.href // Include current URL for proper OG tags 10 11 }); 11 12 12 - return { siteMeta }; 13 + // Fetch lightweight public data for layout using injected fetch 14 + let profile = null; 15 + let siteInfo = null; 16 + 17 + try { 18 + profile = await fetchProfile(fetch); 19 + } catch (err) { 20 + // Non-fatal: layout should still render even if profile fails 21 + console.warn('Layout: failed to fetch profile in load', err); 22 + } 23 + 24 + try { 25 + siteInfo = await fetchSiteInfo(fetch); 26 + } catch (err) { 27 + console.warn('Layout: failed to fetch siteInfo in load', err); 28 + } 29 + 30 + return { siteMeta, profile, siteInfo }; 13 31 };
+3 -3
src/routes/site/meta/+page.ts
··· 3 3 import { createSiteMeta, type SiteMetadata, defaultSiteMeta } from '$lib/helper/siteMeta'; 4 4 import { ogImages } from '$lib/helper/ogImages'; 5 5 6 - export const load: PageLoad = async ({ parent }) => { 6 + export const load: PageLoad = async ({ parent, fetch }) => { 7 7 const { siteMeta } = await parent(); 8 8 9 9 let siteInfo: SiteInfoData | null = null; 10 10 let error: string | null = null; 11 11 12 - try { 13 - siteInfo = await fetchSiteInfo(); 12 + try { 13 + siteInfo = await fetchSiteInfo(fetch); 14 14 } catch (err) { 15 15 error = err instanceof Error ? err.message : 'Failed to load site information'; 16 16 }