your personal website on atproto - mirror blento.app

loadData -> loadDataServer

+256 -17
+33
src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts
··· 31 } 32 return allData; 33 }, 34 minW: 2, 35 minH: 2, 36 name: 'Last.fm Profile',
··· 31 } 32 return allData; 33 }, 34 + loadDataServer: async (items, { cache, env }) => { 35 + const apiKey = env?.LASTFM_API_KEY; 36 + if (!apiKey) return {}; 37 + const allData: Record<string, unknown> = {}; 38 + for (const item of items) { 39 + const username = item.cardData.lastfmUsername; 40 + if (!username) continue; 41 + try { 42 + const cacheKey = `user.getInfo:${username}:7day:50`; 43 + const cached = await cache?.get('lastfm', cacheKey); 44 + if (cached) { 45 + allData[`lastfmProfile:${username}`] = JSON.parse(cached)?.user; 46 + continue; 47 + } 48 + const params = new URLSearchParams({ 49 + method: 'user.getInfo', 50 + user: username, 51 + api_key: apiKey, 52 + format: 'json', 53 + limit: '50' 54 + }); 55 + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); 56 + if (!response.ok) continue; 57 + const data = await response.json(); 58 + if (data.error) continue; 59 + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 12 * 60 * 60); 60 + allData[`lastfmProfile:${username}`] = data?.user; 61 + } catch (error) { 62 + console.error('Failed to fetch Last.fm profile:', error); 63 + } 64 + } 65 + return allData; 66 + }, 67 minW: 2, 68 minH: 2, 69 name: 'Last.fm Profile',
+33
src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts
··· 31 } 32 return allData; 33 }, 34 onUrlHandler: (url, item) => { 35 const username = getLastFMUsername(url); 36 if (!username) return null;
··· 31 } 32 return allData; 33 }, 34 + loadDataServer: async (items, { cache, env }) => { 35 + const apiKey = env?.LASTFM_API_KEY; 36 + if (!apiKey) return {}; 37 + const allData: Record<string, unknown> = {}; 38 + for (const item of items) { 39 + const username = item.cardData.lastfmUsername; 40 + if (!username) continue; 41 + try { 42 + const cacheKey = `user.getRecentTracks:${username}:7day:50`; 43 + const cached = await cache?.get('lastfm', cacheKey); 44 + if (cached) { 45 + allData[`lastfmRecentTracks:${username}`] = JSON.parse(cached)?.recenttracks?.track ?? []; 46 + continue; 47 + } 48 + const params = new URLSearchParams({ 49 + method: 'user.getRecentTracks', 50 + user: username, 51 + api_key: apiKey, 52 + format: 'json', 53 + limit: '50' 54 + }); 55 + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); 56 + if (!response.ok) continue; 57 + const data = await response.json(); 58 + if (data.error) continue; 59 + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 15 * 60); 60 + allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; 61 + } catch (error) { 62 + console.error('Failed to fetch Last.fm recent tracks:', error); 63 + } 64 + } 65 + return allData; 66 + }, 67 onUrlHandler: (url, item) => { 68 const username = getLastFMUsername(url); 69 if (!username) return null;
+36
src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts
··· 35 } 36 return allData; 37 }, 38 allowSetColor: true, 39 defaultColor: 'base', 40 minW: 2,
··· 35 } 36 return allData; 37 }, 38 + loadDataServer: async (items, { cache, env }) => { 39 + const apiKey = env?.LASTFM_API_KEY; 40 + if (!apiKey) return {}; 41 + const allData: Record<string, unknown> = {}; 42 + for (const item of items) { 43 + const username = item.cardData.lastfmUsername; 44 + const period = item.cardData.period ?? '7day'; 45 + if (!username) continue; 46 + try { 47 + const cacheKey = `user.getTopAlbums:${username}:${period}:50`; 48 + const cached = await cache?.get('lastfm', cacheKey); 49 + if (cached) { 50 + allData[`lastfmTopAlbums:${username}:${period}`] = 51 + JSON.parse(cached)?.topalbums?.album ?? []; 52 + continue; 53 + } 54 + const params = new URLSearchParams({ 55 + method: 'user.getTopAlbums', 56 + user: username, 57 + api_key: apiKey, 58 + format: 'json', 59 + limit: '50', 60 + period 61 + }); 62 + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); 63 + if (!response.ok) continue; 64 + const data = await response.json(); 65 + if (data.error) continue; 66 + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 60 * 60); 67 + allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; 68 + } catch (error) { 69 + console.error('Failed to fetch Last.fm top albums:', error); 70 + } 71 + } 72 + return allData; 73 + }, 74 allowSetColor: true, 75 defaultColor: 'base', 76 minW: 2,
+36
src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts
··· 35 } 36 return allData; 37 }, 38 minW: 3, 39 minH: 2, 40 canHaveLabel: true,
··· 35 } 36 return allData; 37 }, 38 + loadDataServer: async (items, { cache, env }) => { 39 + const apiKey = env?.LASTFM_API_KEY; 40 + if (!apiKey) return {}; 41 + const allData: Record<string, unknown> = {}; 42 + for (const item of items) { 43 + const username = item.cardData.lastfmUsername; 44 + const period = item.cardData.period ?? '7day'; 45 + if (!username) continue; 46 + try { 47 + const cacheKey = `user.getTopTracks:${username}:${period}:50`; 48 + const cached = await cache?.get('lastfm', cacheKey); 49 + if (cached) { 50 + allData[`lastfmTopTracks:${username}:${period}`] = 51 + JSON.parse(cached)?.toptracks?.track ?? []; 52 + continue; 53 + } 54 + const params = new URLSearchParams({ 55 + method: 'user.getTopTracks', 56 + user: username, 57 + api_key: apiKey, 58 + format: 'json', 59 + limit: '50', 60 + period 61 + }); 62 + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); 63 + if (!response.ok) continue; 64 + const data = await response.json(); 65 + if (data.error) continue; 66 + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 60 * 60); 67 + allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; 68 + } catch (error) { 69 + console.error('Failed to fetch Last.fm top tracks:', error); 70 + } 71 + } 72 + return allData; 73 + }, 74 minW: 3, 75 minH: 2, 76 canHaveLabel: true,
+26
src/lib/cards/social/GitHubContributorsCard/index.ts
··· 45 } 46 return contributorsData; 47 }, 48 onUrlHandler: (url, item) => { 49 const match = url.match(/github\.com\/([^/]+)\/([^/]+)/); 50 if (!match) return null;
··· 45 } 46 return contributorsData; 47 }, 48 + loadDataServer: async (items, { cache }) => { 49 + const contributorsData: GitHubContributorsLoadedData = {}; 50 + for (const item of items) { 51 + const { owner, repo } = item.cardData; 52 + if (!owner || !repo) continue; 53 + const key = `${owner}/${repo}`; 54 + if (contributorsData[key]) continue; 55 + try { 56 + const cached = await cache?.get('gh-contrib', key); 57 + if (cached) { 58 + contributorsData[key] = JSON.parse(cached); 59 + continue; 60 + } 61 + const response = await fetch( 62 + `https://edge-function-github-contribution.vercel.app/api/github-contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` 63 + ); 64 + if (!response.ok) continue; 65 + const data = await response.json(); 66 + await cache?.put('gh-contrib', key, JSON.stringify(data)); 67 + contributorsData[key] = data; 68 + } catch (error) { 69 + console.error('Failed to fetch GitHub contributors:', error); 70 + } 71 + } 72 + return contributorsData; 73 + }, 74 onUrlHandler: (url, item) => { 75 const match = url.match(/github\.com\/([^/]+)\/([^/]+)/); 76 if (!match) return null;
+26
src/lib/cards/social/GitHubProfileCard/index.ts
··· 27 } 28 return githubData; 29 }, 30 onUrlHandler: (url, item) => { 31 const username = getGitHubUsername(url); 32
··· 27 } 28 return githubData; 29 }, 30 + loadDataServer: async (items, { cache }) => { 31 + const githubData: Record<string, GitHubContributionsData> = {}; 32 + for (const item of items) { 33 + const user = item.cardData.user; 34 + if (!user) continue; 35 + try { 36 + const cached = await cache?.get('github', user); 37 + if (cached) { 38 + githubData[user] = JSON.parse(cached); 39 + continue; 40 + } 41 + const response = await fetch( 42 + `https://edge-function-github-contribution.vercel.app/api/github-data?user=${encodeURIComponent(user)}` 43 + ); 44 + if (!response.ok) continue; 45 + const data = await response.json(); 46 + if (!data?.user) continue; 47 + const result = data.user as GitHubContributionsData; 48 + await cache?.put('github', user, JSON.stringify(result)); 49 + githubData[user] = result; 50 + } catch (error) { 51 + console.error('Failed to fetch GitHub contributions:', error); 52 + } 53 + } 54 + return githubData; 55 + }, 56 onUrlHandler: (url, item) => { 57 const username = getGitHubUsername(url); 58
+16
src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts
··· 15 const data = await res.json(); 16 return data; 17 }, 18 minW: 3, 19 canHaveLabel: true, 20
··· 15 const data = await res.json(); 16 return data; 17 }, 18 + loadDataServer: async (_items, { cache }) => { 19 + try { 20 + const cached = await cache?.get('npmx', 'likes'); 21 + if (cached) return JSON.parse(cached); 22 + const response = await fetch( 23 + 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20' 24 + ); 25 + if (!response.ok) return undefined; 26 + const data = await response.json(); 27 + await cache?.put('npmx', 'likes', JSON.stringify(data)); 28 + return data; 29 + } catch (error) { 30 + console.error('Error fetching npmx leaderboard:', error); 31 + return undefined; 32 + } 33 + }, 34 minW: 3, 35 canHaveLabel: true, 36
+17
src/lib/cards/types.ts
··· 40 { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } 41 ) => Promise<unknown>; 42 43 // show color selection popup 44 allowSetColor?: boolean; 45
··· 40 { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } 41 ) => Promise<unknown>; 42 43 + // server-side version of loadData that calls external APIs directly 44 + // instead of going through self-referential /api/ routes (avoids 522 on Cloudflare Workers) 45 + loadDataServer?: ( 46 + items: Item[], 47 + { 48 + did, 49 + handle, 50 + cache, 51 + env 52 + }: { 53 + did: Did; 54 + handle: string; 55 + cache?: CacheService; 56 + env?: Record<string, string | undefined>; 57 + } 58 + ) => Promise<unknown>; 59 + 60 // show color selection popup 61 allowSetColor?: boolean; 62
+11 -6
src/lib/website/load.ts
··· 44 handle: ActorIdentifier, 45 cache: CacheService | undefined, 46 forceUpdate: boolean = false, 47 - page: string = 'self' 48 ): Promise<WebsiteData> { 49 if (!handle) throw error(404); 50 if (handle === 'favicon.ico') throw error(404); ··· 94 for (const cardType of cardTypesArray) { 95 const cardDef = CardDefinitionsByType[cardType]; 96 97 - if (!cardDef?.loadData) continue; 98 99 try { 100 - additionDataPromises[cardType] = cardDef.loadData( 101 - cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[], 102 - loadOptions 103 - ); 104 } catch { 105 console.error('error getting additional data for', cardType); 106 }
··· 44 handle: ActorIdentifier, 45 cache: CacheService | undefined, 46 forceUpdate: boolean = false, 47 + page: string = 'self', 48 + env?: Record<string, string | undefined> 49 ): Promise<WebsiteData> { 50 if (!handle) throw error(404); 51 if (handle === 'favicon.ico') throw error(404); ··· 95 for (const cardType of cardTypesArray) { 96 const cardDef = CardDefinitionsByType[cardType]; 97 98 + const items = cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[]; 99 100 try { 101 + if (cardDef?.loadDataServer) { 102 + additionDataPromises[cardType] = cardDef.loadDataServer(items, { 103 + ...loadOptions, 104 + env 105 + }); 106 + } else if (cardDef?.loadData) { 107 + additionDataPromises[cardType] = cardDef.loadData(items, loadOptions); 108 + } 109 } catch { 110 console.error('error getting additional data for', cardType); 111 }
+3 -2
src/routes/+page.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 import { createCache } from '$lib/cache'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 ··· 15 try { 16 const did = await kv.get(customDomain); 17 18 - if (did) return await loadData(did as ActorIdentifier, cache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 - return await loadData(handle as ActorIdentifier, cache); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 import { createCache } from '$lib/cache'; 5 import type { ActorIdentifier } from '@atcute/lexicons'; 6 ··· 16 try { 17 const did = await kv.get(customDomain); 18 19 + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); 20 } catch (error) { 21 console.error('failed to get custom domain kv', error); 22 } 23 } 24 25 + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); 26 }
+1 -1
src/routes/[actor=actor]/(pages)/+layout.server.ts
··· 9 10 const cache = createCache(platform); 11 12 - return await loadData(params.actor, cache, false, params.page); 13 }
··· 9 10 const cache = createCache(platform); 11 12 + return await loadData(params.actor, cache, false, params.page, env); 13 }
+2 -1
src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { createCache } from '$lib/cache'; 3 4 import { error } from '@sveltejs/kit'; 5 import { text } from '@sveltejs/kit'; ··· 7 export async function GET({ params, platform }) { 8 const cache = createCache(platform); 9 10 - const data = await loadData(params.actor, cache, false, params.page); 11 12 if (!data.publication) throw error(300); 13
··· 1 import { loadData } from '$lib/website/load'; 2 import { createCache } from '$lib/cache'; 3 + import { env } from '$env/dynamic/private'; 4 5 import { error } from '@sveltejs/kit'; 6 import { text } from '@sveltejs/kit'; ··· 8 export async function GET({ params, platform }) { 9 const cache = createCache(platform); 10 11 + const data = await loadData(params.actor, cache, false, params.page, env); 12 13 if (!data.publication) throw error(300); 14
+2 -1
src/routes/[actor=actor]/api/refresh/+server.ts
··· 1 import { createCache } from '$lib/cache'; 2 import { loadData } from '$lib/website/load.js'; 3 import type { Handle } from '@atcute/lexicons'; 4 import { json } from '@sveltejs/kit'; 5 ··· 7 const cache = createCache(platform); 8 if (!cache) return json('no cache'); 9 10 - await loadData(params.actor, cache, true); 11 12 return json('ok'); 13 }
··· 1 import { createCache } from '$lib/cache'; 2 import { loadData } from '$lib/website/load.js'; 3 + import { env } from '$env/dynamic/private'; 4 import type { Handle } from '@atcute/lexicons'; 5 import { json } from '@sveltejs/kit'; 6 ··· 8 const cache = createCache(platform); 9 if (!cache) return json('no cache'); 10 11 + await loadData(params.actor, cache, true, 'self', env); 12 13 return json('ok'); 14 }
+2 -1
src/routes/[actor=actor]/og.png/+server.ts
··· 1 import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 import { createCache } from '$lib/cache'; 3 import { loadData } from '$lib/website/load'; 4 import type { Handle } from '@atcute/lexicons'; 5 import { ImageResponse } from '@ethercorps/sveltekit-og'; 6 ··· 16 export async function GET({ params, platform }) { 17 const cache = createCache(platform); 18 19 - const data = await loadData(params.actor, cache); 20 21 let image: string | undefined = data.profile.avatar; 22
··· 1 import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; 2 import { createCache } from '$lib/cache'; 3 import { loadData } from '$lib/website/load'; 4 + import { env } from '$env/dynamic/private'; 5 import type { Handle } from '@atcute/lexicons'; 6 import { ImageResponse } from '@ethercorps/sveltekit-og'; 7 ··· 17 export async function GET({ params, platform }) { 18 const cache = createCache(platform); 19 20 + const data = await loadData(params.actor, cache, false, 'self', env); 21 22 let image: string | undefined = data.profile.avatar; 23
+2 -1
src/routes/api/update/+server.ts
··· 1 import { createCache } from '$lib/cache'; 2 import { getCache, loadData } from '$lib/website/load'; 3 import { json } from '@sveltejs/kit'; 4 import type { AppBskyActorDefs } from '@atcute/bluesky'; 5 ··· 20 21 try { 22 const cached = await getCache(handle, 'self', cache); 23 - if (!cached) await loadData(handle, cache, true); 24 } catch (error) { 25 console.error(error); 26 return json('error');
··· 1 import { createCache } from '$lib/cache'; 2 import { getCache, loadData } from '$lib/website/load'; 3 + import { env } from '$env/dynamic/private'; 4 import { json } from '@sveltejs/kit'; 5 import type { AppBskyActorDefs } from '@atcute/bluesky'; 6 ··· 21 22 try { 23 const cached = await getCache(handle, 'self', cache); 24 + if (!cached) await loadData(handle, cache, true, 'self', env); 25 } catch (error) { 26 console.error(error); 27 return json('error');
+3 -2
src/routes/edit/+page.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 import { createCache } from '$lib/cache'; 4 import type { ActorIdentifier } from '@atcute/lexicons'; 5 ··· 15 try { 16 const did = await kv.get(customDomain); 17 18 - if (did) return await loadData(did as ActorIdentifier, cache); 19 } catch (error) { 20 console.error('failed to get custom domain kv', error); 21 } 22 } 23 24 - return await loadData(handle as ActorIdentifier, cache); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 import { createCache } from '$lib/cache'; 5 import type { ActorIdentifier } from '@atcute/lexicons'; 6 ··· 16 try { 17 const did = await kv.get(customDomain); 18 19 + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); 20 } catch (error) { 21 console.error('failed to get custom domain kv', error); 22 } 23 } 24 25 + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); 26 }
+3 -2
src/routes/p/[[page]]/+layout.server.ts
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 import { createCache } from '$lib/cache'; 4 import type { Did, Handle } from '@atcute/lexicons'; 5 ··· 15 if (kv && customDomain) { 16 try { 17 const did = await kv.get(customDomain); 18 - return await loadData(did as Did, cache, false, params.page); 19 } catch { 20 console.error('failed'); 21 } 22 } 23 24 - return await loadData(handle as Handle, cache, false, params.page); 25 }
··· 1 import { loadData } from '$lib/website/load'; 2 import { env } from '$env/dynamic/public'; 3 + import { env as privateEnv } from '$env/dynamic/private'; 4 import { createCache } from '$lib/cache'; 5 import type { Did, Handle } from '@atcute/lexicons'; 6 ··· 16 if (kv && customDomain) { 17 try { 18 const did = await kv.get(customDomain); 19 + return await loadData(did as Did, cache, false, params.page, privateEnv); 20 } catch { 21 console.error('failed'); 22 } 23 } 24 25 + return await loadData(handle as Handle, cache, false, params.page, privateEnv); 26 }
+4
svelte.config.js
··· 11 adapter: adapter(), 12 paths: { 13 base: '' 14 } 15 }, 16 compilerOptions: { 17 experimental: { 18 async: true
··· 11 adapter: adapter(), 12 paths: { 13 base: '' 14 + }, 15 + experimental: { 16 + remoteFunctions: true 17 } 18 }, 19 + 20 compilerOptions: { 21 experimental: { 22 async: true