diff --git a/CLAUDE.md b/CLAUDE.md index 3e7480d..39b2963 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,9 +66,20 @@ Grid margins: 16px desktop, 12px mobile. - Data is stored in user's PDS under collection `app.blento.card` - **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers. +**Caching (`src/lib/cache.ts`):** + +- `CacheService` class wraps a single Cloudflare KV namespace (`USER_DATA_CACHE`) with namespaced keys +- Keys are stored as `namespace:key` (e.g. `user:did:plc:abc`, `github:someuser`, `lastfm:method:user:period:limit`) +- Per-namespace default TTLs via KV `expirationTtl`: `user` (24h), `identity` (7d), `github` (12h), `gh-contrib` (12h), `lastfm` (1h, overridable), `npmx` (12h), `meta` (no expiry) +- User data is keyed by DID with bidirectional handle↔DID identity mappings (`identity:h:{handle}` → DID, `identity:d:{did}` → handle) +- `getUser(identifier)` accepts either a handle or DID and resolves automatically +- `putUser(did, handle, data)` writes data + both identity mappings +- `createCache(platform)` factory function creates a `CacheService` from `platform.env` +- `CUSTOM_DOMAINS` KV namespace is separate and accessed directly for custom domain → DID resolution + **Data Loading (`src/lib/website/`):** -- `load.ts` - Fetches user data from their PDS, with optional KV caching via `UserCache` +- `load.ts` - Fetches user data from their PDS, with optional caching via `CacheService` - `data.ts` - Defines which collections/records to fetch - `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree diff --git a/src/lib/cache.ts b/src/lib/cache.ts new file mode 100644 index 0000000..d509d7a --- /dev/null +++ b/src/lib/cache.ts @@ -0,0 +1,93 @@ +import type { KVNamespace } from '@cloudflare/workers-types'; + +/** TTL in seconds for each cache namespace */ +const NAMESPACE_TTL = { + user: 60 * 60 * 24, // 24 hours + identity: 60 * 60 * 24 * 7, // 7 days + github: 60 * 60 * 12, // 12 hours + 'gh-contrib': 60 * 60 * 12, // 12 hours + lastfm: 60 * 60, // 1 hour (default, overridable per-put) + npmx: 60 * 60 * 12, // 12 hours + meta: 0 // no auto-expiry +} as const; + +export type CacheNamespace = keyof typeof NAMESPACE_TTL; + +export class CacheService { + constructor(private kv: KVNamespace) {} + + // === Generic namespaced operations === + + async get(namespace: CacheNamespace, key: string): Promise { + return this.kv.get(`${namespace}:${key}`); + } + + async put(namespace: CacheNamespace, key: string, value: string, ttlSeconds?: number): Promise { + const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0; + await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined); + } + + async delete(namespace: CacheNamespace, key: string): Promise { + await this.kv.delete(`${namespace}:${key}`); + } + + async list(namespace: CacheNamespace): Promise { + const prefix = `${namespace}:`; + const result = await this.kv.list({ prefix }); + return result.keys.map((k) => k.name.slice(prefix.length)); + } + + // === JSON convenience === + + async getJSON(namespace: CacheNamespace, key: string): Promise { + const raw = await this.get(namespace, key); + if (!raw) return null; + return JSON.parse(raw) as T; + } + + async putJSON( + namespace: CacheNamespace, + key: string, + value: unknown, + ttlSeconds?: number + ): Promise { + await this.put(namespace, key, JSON.stringify(value), ttlSeconds); + } + + // === User data (keyed by DID, with handle↔did resolution) === + + async getUser(identifier: string): Promise { + const did = await this.resolveDid(identifier); + if (!did) return null; + return this.get('user', did); + } + + async putUser(did: string, handle: string, data: string): Promise { + await Promise.all([ + this.put('user', did, data), + this.put('identity', `h:${handle}`, did), + this.put('identity', `d:${did}`, handle) + ]); + } + + async listUsers(): Promise { + return this.list('user'); + } + + // === Identity resolution === + + async resolveDid(identifier: string): Promise { + if (identifier.startsWith('did:')) return identifier; + return this.get('identity', `h:${identifier}`); + } + + async resolveHandle(did: string): Promise { + return this.get('identity', `d:${did}`); + } +} + +export function createCache(platform?: App.Platform): CacheService | undefined { + const kv = platform?.env?.USER_DATA_CACHE; + if (!kv) return undefined; + return new CacheService(kv); +} diff --git a/src/lib/cards/special/UpdatedBlentos/index.ts b/src/lib/cards/special/UpdatedBlentos/index.ts index 0a2f952..ccd611e 100644 --- a/src/lib/cards/special/UpdatedBlentos/index.ts +++ b/src/lib/cards/special/UpdatedBlentos/index.ts @@ -15,7 +15,7 @@ export const UpdatedBlentosCardDefitition = { 'https://ufos-api.microcosm.blue/records?collection=app.blento.card' ); const recentRecords = await response.json(); - const existingUsers = await cache?.get('updatedBlentos'); + const existingUsers = await cache?.get('meta', 'updatedBlentos'); const existingUsersArray: ProfileWithBlentoFlag[] = existingUsers ? JSON.parse(existingUsers) : []; @@ -50,9 +50,8 @@ export const UpdatedBlentosCardDefitition = { (v) => v && v.handle !== 'handle.invalid' && !v.handle.endsWith('.pds.rip') ); - if (cache) { - await cache?.put('updatedBlentos', JSON.stringify(result)); - } + await cache?.put('meta', 'updatedBlentos', JSON.stringify(result)); + return JSON.parse(JSON.stringify(result.slice(0, 20))); } catch (error) { console.error('error fetching updated blentos', error); diff --git a/src/lib/cards/types.ts b/src/lib/cards/types.ts index a79996e..995fa24 100644 --- a/src/lib/cards/types.ts +++ b/src/lib/cards/types.ts @@ -1,5 +1,6 @@ import type { Component } from 'svelte'; -import type { Item, UserCache } from '$lib/types'; +import type { Item } from '$lib/types'; +import type { CacheService } from '$lib/cache'; import type { Did } from '@atcute/lexicons'; export type CreationModalComponentProps = { @@ -36,7 +37,7 @@ export type CardDefinition = { loadData?: ( // all cards of that type items: Item[], - { did, handle, cache }: { did: Did; handle: string; cache?: UserCache } + { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } ) => Promise; // show color selection popup diff --git a/src/lib/types.ts b/src/lib/types.ts index fe1166d..b4dfa50 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -66,8 +66,3 @@ export type WebsiteData = { updatedAt: number; version?: number; }; - -export type UserCache = { - get: (key: string) => string; - put: (key: string, value: string) => void; -}; diff --git a/src/lib/website/load.ts b/src/lib/website/load.ts index f52407a..dab7ca2 100644 --- a/src/lib/website/load.ts +++ b/src/lib/website/load.ts @@ -1,6 +1,7 @@ import { getDetailedProfile, listRecords, resolveHandle, parseUri, getRecord } from '$lib/atproto'; import { CardDefinitionsByType } from '$lib/cards'; -import type { Item, UserCache, WebsiteData } from '$lib/types'; +import type { CacheService } from '$lib/cache'; +import type { Item, WebsiteData } from '$lib/types'; import { error } from '@sveltejs/kit'; import type { ActorIdentifier, Did } from '@atcute/lexicons'; @@ -9,27 +10,18 @@ import { fixAllCollisions, compactItems } from '$lib/layout'; const CURRENT_CACHE_VERSION = 1; -export async function getCache(handle: string, page: string, cache?: UserCache) { +export async function getCache(identifier: string, page: string, cache?: CacheService) { try { - const cachedResult = await cache?.get?.(handle); + const cachedResult = await cache?.getUser(identifier); if (!cachedResult) return; const result = JSON.parse(cachedResult); - const update = result.updatedAt; - const timePassed = (Date.now() - update) / 1000; - - const ONE_DAY = 60 * 60 * 24; if (!result.version || result.version !== CURRENT_CACHE_VERSION) { console.log('skipping cache because of version mismatch'); return; } - if (timePassed > ONE_DAY) { - console.log('skipping cache because of age'); - return; - } - result.page = 'blento.' + page; result.publication = (result.publications as Awaited>).find( @@ -42,7 +34,6 @@ export async function getCache(handle: string, page: string, cache?: UserCache) delete result['publications']; - console.log('using cached result for handle', handle, 'last update', timePassed, 'seconds ago'); return checkData(result); } catch (error) { console.log('getting cached result failed', error); @@ -51,7 +42,7 @@ export async function getCache(handle: string, page: string, cache?: UserCache) export async function loadData( handle: ActorIdentifier, - cache: UserCache | undefined, + cache: CacheService | undefined, forceUpdate: boolean = false, page: string = 'self' ): Promise { @@ -74,8 +65,8 @@ export async function loadData( } const [cards, mainPublication, pages, profile] = await Promise.all([ - listRecords({ did, collection: 'app.blento.card' }).catch(() => { - console.error('error getting records for collection app.blento.card'); + listRecords({ did, collection: 'app.blento.card' }).catch((e) => { + console.error('error getting records for collection app.blento.card', e); return [] as Awaited>; }), getRecord({ @@ -140,8 +131,11 @@ export async function loadData( version: CURRENT_CACHE_VERSION }; - const stringifiedResult = JSON.stringify(result); - await cache?.put?.(handle, stringifiedResult); + // Only cache results that have cards to avoid caching PDS errors + if (result.cards.length > 0) { + const stringifiedResult = JSON.stringify(result); + await cache?.putUser(did, handle as string, stringifiedResult); + } const parsedResult = structuredClone(result) as any; diff --git a/src/params/handle.ts b/src/params/actor.ts similarity index 100% rename from src/params/handle.ts rename to src/params/actor.ts diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index c571bf2..add0f2c 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,6 +1,6 @@ import { loadData } from '$lib/website/load'; import { env } from '$env/dynamic/public'; -import type { UserCache } from '$lib/types'; +import { createCache } from '$lib/cache'; import type { ActorIdentifier } from '@atcute/lexicons'; export async function load({ platform, request }) { @@ -8,18 +8,18 @@ export async function load({ platform, request }) { const kv = platform?.env?.CUSTOM_DOMAINS; - const cache = platform?.env?.USER_DATA_CACHE as unknown; + const cache = createCache(platform); const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); if (kv && customDomain) { try { const did = await kv.get(customDomain); - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); + if (did) return await loadData(did as ActorIdentifier, cache); } catch (error) { console.error('failed to get custom domain kv', error); } } - return await loadData(handle as ActorIdentifier, cache as UserCache); + return await loadData(handle as ActorIdentifier, cache); } diff --git a/src/routes/[handle=handle]/(pages)/+layout.server.ts b/src/routes/[actor=actor]/(pages)/+layout.server.ts similarity index 58% rename from src/routes/[handle=handle]/(pages)/+layout.server.ts rename to src/routes/[actor=actor]/(pages)/+layout.server.ts index f601cb5..79a5167 100644 --- a/src/routes/[handle=handle]/(pages)/+layout.server.ts +++ b/src/routes/[actor=actor]/(pages)/+layout.server.ts @@ -1,13 +1,13 @@ import { loadData } from '$lib/website/load'; import { env } from '$env/dynamic/private'; import { error } from '@sveltejs/kit'; -import type { UserCache } from '$lib/types'; +import { createCache } from '$lib/cache'; import type { Handle } from '@atcute/lexicons'; export async function load({ params, platform }) { if (env.PUBLIC_IS_SELFHOSTED) error(404); - const cache = platform?.env?.USER_DATA_CACHE as unknown; + const cache = createCache(platform); - return await loadData(params.handle as Handle, cache as UserCache, false, params.page); + return await loadData(params.actor, cache, false, params.page); } diff --git a/src/routes/[handle=handle]/(pages)/+page.svelte b/src/routes/[actor=actor]/(pages)/+page.svelte similarity index 100% rename from src/routes/[handle=handle]/(pages)/+page.svelte rename to src/routes/[actor=actor]/(pages)/+page.svelte diff --git a/src/routes/[handle=handle]/(pages)/edit/+page.svelte b/src/routes/[actor=actor]/(pages)/edit/+page.svelte similarity index 100% rename from src/routes/[handle=handle]/(pages)/edit/+page.svelte rename to src/routes/[actor=actor]/(pages)/edit/+page.svelte diff --git a/src/routes/[handle=handle]/(pages)/p/[[page]]/+page.svelte b/src/routes/[actor=actor]/(pages)/p/[[page]]/+page.svelte similarity index 100% rename from src/routes/[handle=handle]/(pages)/p/[[page]]/+page.svelte rename to src/routes/[actor=actor]/(pages)/p/[[page]]/+page.svelte diff --git a/src/routes/[handle=handle]/(pages)/p/[[page]]/copy/+page.svelte b/src/routes/[actor=actor]/(pages)/p/[[page]]/copy/+page.svelte similarity index 100% rename from src/routes/[handle=handle]/(pages)/p/[[page]]/copy/+page.svelte rename to src/routes/[actor=actor]/(pages)/p/[[page]]/copy/+page.svelte diff --git a/src/routes/[handle=handle]/(pages)/p/[[page]]/edit/+page.svelte b/src/routes/[actor=actor]/(pages)/p/[[page]]/edit/+page.svelte similarity index 100% rename from src/routes/[handle=handle]/(pages)/p/[[page]]/edit/+page.svelte rename to src/routes/[actor=actor]/(pages)/p/[[page]]/edit/+page.svelte diff --git a/src/routes/[handle=handle]/.well-known/site.standard.publication/+server.ts b/src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts similarity index 60% rename from src/routes/[handle=handle]/.well-known/site.standard.publication/+server.ts rename to src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts index e1217e8..63bfa6e 100644 --- a/src/routes/[handle=handle]/.well-known/site.standard.publication/+server.ts +++ b/src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts @@ -1,12 +1,13 @@ import { loadData } from '$lib/website/load'; +import { createCache } from '$lib/cache'; + import { error } from '@sveltejs/kit'; -import type { UserCache } from '$lib/types'; import { text } from '@sveltejs/kit'; export async function GET({ params, platform }) { - const cache = platform?.env?.USER_DATA_CACHE as unknown; + const cache = createCache(platform); - const data = await loadData(params.handle, cache as UserCache, false, params.page); + const data = await loadData(params.actor, cache, false, params.page); if (!data.publication) throw error(300); diff --git a/src/routes/[actor=actor]/api/refresh/+server.ts b/src/routes/[actor=actor]/api/refresh/+server.ts new file mode 100644 index 0000000..2a315fe --- /dev/null +++ b/src/routes/[actor=actor]/api/refresh/+server.ts @@ -0,0 +1,13 @@ +import { createCache } from '$lib/cache'; +import { loadData } from '$lib/website/load.js'; +import type { Handle } from '@atcute/lexicons'; +import { json } from '@sveltejs/kit'; + +export async function GET({ params, platform }) { + const cache = createCache(platform); + if (!cache) return json('no cache'); + + await loadData(params.actor, cache, true); + + return json('ok'); +} diff --git a/src/routes/[handle=handle]/og.png/+server.ts b/src/routes/[actor=actor]/og.png/+server.ts similarity index 89% rename from src/routes/[handle=handle]/og.png/+server.ts rename to src/routes/[actor=actor]/og.png/+server.ts index d972f47..122d800 100644 --- a/src/routes/[handle=handle]/og.png/+server.ts +++ b/src/routes/[actor=actor]/og.png/+server.ts @@ -1,8 +1,7 @@ import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; -import type { UserCache } from '$lib/types'; +import { createCache } from '$lib/cache'; import { loadData } from '$lib/website/load'; import type { Handle } from '@atcute/lexicons'; -import { isDid } from '@atcute/lexicons/syntax'; import { ImageResponse } from '@ethercorps/sveltekit-og'; function escapeHtml(str: string): string { @@ -15,9 +14,9 @@ function escapeHtml(str: string): string { } export async function GET({ params, platform }) { - const cache = platform?.env?.USER_DATA_CACHE as unknown; + const cache = createCache(platform); - const data = await loadData(params.handle as Handle, cache as UserCache); + const data = await loadData(params.actor, cache); let image: string | undefined = data.profile.avatar; diff --git a/src/routes/[handle=handle]/api/refresh/+server.ts b/src/routes/[handle=handle]/api/refresh/+server.ts deleted file mode 100644 index fb47780..0000000 --- a/src/routes/[handle=handle]/api/refresh/+server.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { UserCache } from '$lib/types'; -import { loadData } from '$lib/website/load.js'; -import type { Handle } from '@atcute/lexicons'; -import { json } from '@sveltejs/kit'; - -export async function GET({ params, platform }) { - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); - const handle = params.handle; - - const cache = platform?.env?.USER_DATA_CACHE as unknown; - await loadData(handle as Handle, cache as UserCache, true); - - return json('ok'); -} diff --git a/src/routes/all/+page.server.ts b/src/routes/all/+page.server.ts deleted file mode 100644 index 92a6a69..0000000 --- a/src/routes/all/+page.server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { env } from '$env/dynamic/public'; -import type { UserCache, WebsiteData } from '$lib/types.js'; -import { loadData } from '$lib/website/load'; -import type { Handle } from '@atcute/lexicons'; -import type { AppBskyActorDefs } from '@atcute/bluesky'; - -export async function load({ platform }) { - const cache = platform?.env?.USER_DATA_CACHE; - - const list = await cache?.list(); - - const profiles: AppBskyActorDefs.ProfileViewDetailed[] = []; - for (const value of list?.keys ?? []) { - // check if at least one card - const result = await cache?.get(value.name); - if (!result) continue; - const parsed = JSON.parse(result) as WebsiteData; - - if (parsed.version !== 1 || !parsed.cards?.length) continue; - - profiles.push(parsed.profile); - } - - profiles.sort((a, b) => a.handle.localeCompare(b.handle)); - - const handle = env.PUBLIC_HANDLE; - - const data = await loadData(handle as Handle, cache as unknown as UserCache); - - data.publication ??= {}; - data.publication.preferences ??= {}; - data.publication.preferences.hideProfileSection = true; - - return { ...data, profiles }; -} diff --git a/src/routes/all/+page.svelte b/src/routes/all/+page.svelte deleted file mode 100644 index 923f914..0000000 --- a/src/routes/all/+page.svelte +++ /dev/null @@ -1,29 +0,0 @@ - - - { - const card = createEmptyCard(''); - card.cardType = 'blueskyProfile'; - card.cardData = { - avatar: v.avatar, - handle: v.handle, - displayName: v.displayName - }; - - card.x = (i % 4) * 2; - card.y = Math.floor(i / 4) * 2; - - card.mobileX = (i % 2) * 4; - card.mobileY = Math.floor(i / 2) * 4; - - return card; - }) - }} -/> diff --git a/src/routes/api/github/+server.ts b/src/routes/api/github/+server.ts index 5089cbd..1bcb6dc 100644 --- a/src/routes/api/github/+server.ts +++ b/src/routes/api/github/+server.ts @@ -1,6 +1,7 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import type { GitHubContributionsData } from '$lib/cards/social/GitHubProfileCard/types'; +import { createCache } from '$lib/cache'; const GithubAPIURL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; @@ -11,17 +12,11 @@ export const GET: RequestHandler = async ({ url, platform }) => { return json({ error: 'No user provided' }, { status: 400 }); } - const cachedData = await platform?.env?.USER_DATA_CACHE?.get('#github:' + user); + const cache = createCache(platform); + const cachedData = await cache?.get('github', user); if (cachedData) { - const parsedCache = JSON.parse(cachedData); - - const TWELVE_HOURS = 12 * 60 * 60 * 1000; - const now = Date.now(); - - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { - return json(parsedCache); - } + return json(JSON.parse(cachedData)); } try { @@ -42,9 +37,8 @@ export const GET: RequestHandler = async ({ url, platform }) => { } const result = data.user as GitHubContributionsData; - result.updatedAt = Date.now(); - await platform?.env?.USER_DATA_CACHE?.put('#github:' + user, JSON.stringify(result)); + await cache?.put('github', user, JSON.stringify(result)); return json(result); } catch (error) { diff --git a/src/routes/api/github/contributors/+server.ts b/src/routes/api/github/contributors/+server.ts index 6e81ec8..7f28736 100644 --- a/src/routes/api/github/contributors/+server.ts +++ b/src/routes/api/github/contributors/+server.ts @@ -1,5 +1,6 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; +import { createCache } from '$lib/cache'; const GithubContributorsAPIURL = 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; @@ -12,18 +13,12 @@ export const GET: RequestHandler = async ({ url, platform }) => { return json({ error: 'Missing owner or repo parameter' }, { status: 400 }); } - const cacheKey = `#github-contributors:${owner}/${repo}`; - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); + const cache = createCache(platform); + const cacheKey = `${owner}/${repo}`; + const cachedData = await cache?.get('gh-contrib', cacheKey); if (cachedData) { - const parsedCache = JSON.parse(cachedData); - - const TWELVE_HOURS = 12 * 60 * 60 * 1000; - const now = Date.now(); - - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { - return json(parsedCache.data); - } + return json(JSON.parse(cachedData)); } try { @@ -40,10 +35,7 @@ export const GET: RequestHandler = async ({ url, platform }) => { const data = await response.json(); - await platform?.env?.USER_DATA_CACHE?.put( - cacheKey, - JSON.stringify({ data, updatedAt: Date.now() }) - ); + await cache?.put('gh-contrib', cacheKey, JSON.stringify(data)); return json(data); } catch (error) { diff --git a/src/routes/api/lastfm/+server.ts b/src/routes/api/lastfm/+server.ts index 522330f..e40d478 100644 --- a/src/routes/api/lastfm/+server.ts +++ b/src/routes/api/lastfm/+server.ts @@ -1,6 +1,7 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; import { env } from '$env/dynamic/private'; +import { createCache } from '$lib/cache'; const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; @@ -12,10 +13,10 @@ const ALLOWED_METHODS = [ ]; const CACHE_TTL: Record = { - 'user.getRecentTracks': 15 * 60 * 1000, - 'user.getTopTracks': 60 * 60 * 1000, - 'user.getTopAlbums': 60 * 60 * 1000, - 'user.getInfo': 12 * 60 * 60 * 1000 + 'user.getRecentTracks': 15 * 60, + 'user.getTopTracks': 60 * 60, + 'user.getTopAlbums': 60 * 60, + 'user.getInfo': 12 * 60 * 60 }; export const GET: RequestHandler = async ({ url, platform }) => { @@ -32,16 +33,12 @@ export const GET: RequestHandler = async ({ url, platform }) => { return json({ error: 'Method not allowed' }, { status: 400 }); } - const cacheKey = `#lastfm:${method}:${user}:${period}:${limit}`; - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); + const cache = createCache(platform); + const cacheKey = `${method}:${user}:${period}:${limit}`; + const cachedData = await cache?.get('lastfm', cacheKey); if (cachedData) { - const parsed = JSON.parse(cachedData); - const ttl = CACHE_TTL[method] || 60 * 60 * 1000; - - if (Date.now() - (parsed._cachedAt || 0) < ttl) { - return json(parsed); - } + return json(JSON.parse(cachedData)); } const apiKey = env?.LASTFM_API_KEY; @@ -77,9 +74,8 @@ export const GET: RequestHandler = async ({ url, platform }) => { return json({ error: data.message || 'Last.fm API error' }, { status: 400 }); } - data._cachedAt = Date.now(); - - await platform?.env?.USER_DATA_CACHE?.put(cacheKey, JSON.stringify(data)); + const ttl = CACHE_TTL[method] || 60 * 60; + await cache?.put('lastfm', cacheKey, JSON.stringify(data), ttl); return json(data); } catch (error) { diff --git a/src/routes/api/npmx-leaderboard/+server.ts b/src/routes/api/npmx-leaderboard/+server.ts index 068e80c..17711e7 100644 --- a/src/routes/api/npmx-leaderboard/+server.ts +++ b/src/routes/api/npmx-leaderboard/+server.ts @@ -1,22 +1,16 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; +import { createCache } from '$lib/cache'; const LEADERBOARD_API_URL = 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; export const GET: RequestHandler = async ({ platform }) => { - const cacheKey = '#npmx-leaderboard:likes'; - const cachedData = await platform?.env?.USER_DATA_CACHE?.get(cacheKey); + const cache = createCache(platform); + const cachedData = await cache?.get('npmx', 'likes'); if (cachedData) { - const parsedCache = JSON.parse(cachedData); - - const TWELVE_HOURS = 12 * 60 * 60 * 1000; - const now = Date.now(); - - if (now - (parsedCache.updatedAt || 0) < TWELVE_HOURS) { - return json(parsedCache.data); - } + return json(JSON.parse(cachedData)); } try { @@ -31,10 +25,7 @@ export const GET: RequestHandler = async ({ platform }) => { const data = await response.json(); - await platform?.env?.USER_DATA_CACHE?.put( - cacheKey, - JSON.stringify({ data, updatedAt: Date.now() }) - ); + await cache?.put('npmx', 'likes', JSON.stringify(data)); return json(data); } catch (error) { diff --git a/src/routes/api/reloadRecent/+server.ts b/src/routes/api/reloadRecent/+server.ts index c47aafa..f62ad74 100644 --- a/src/routes/api/reloadRecent/+server.ts +++ b/src/routes/api/reloadRecent/+server.ts @@ -1,10 +1,13 @@ import { getDetailedProfile } from '$lib/atproto'; +import { createCache } from '$lib/cache'; import { json } from '@sveltejs/kit'; import type { AppBskyActorDefs } from '@atcute/bluesky'; export async function GET({ platform }) { - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); - const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); + const cache = createCache(platform); + if (!cache) return json('no cache'); + + const existingUsers = await cache.get('meta', 'updatedBlentos'); const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers ? JSON.parse(existingUsers) @@ -21,7 +24,7 @@ export async function GET({ platform }) { const newProfiles = await Promise.all(newProfilesPromises); - await platform?.env?.USER_DATA_CACHE.put('updatedBlentos', JSON.stringify(newProfiles)); + await cache.put('meta', 'updatedBlentos', JSON.stringify(newProfiles)); return json('ok'); } diff --git a/src/routes/api/update/+server.ts b/src/routes/api/update/+server.ts index 5d384f6..c35560a 100644 --- a/src/routes/api/update/+server.ts +++ b/src/routes/api/update/+server.ts @@ -1,11 +1,13 @@ -import type { UserCache } from '$lib/types'; +import { createCache } from '$lib/cache'; import { getCache, loadData } from '$lib/website/load'; -import type { AppBskyActorDefs } from '@atcute/bluesky'; import { json } from '@sveltejs/kit'; +import type { AppBskyActorDefs } from '@atcute/bluesky'; export async function GET({ platform }) { - if (!platform?.env?.USER_DATA_CACHE) return json('no cache'); - const existingUsers = await platform?.env?.USER_DATA_CACHE?.get('updatedBlentos'); + const cache = createCache(platform); + if (!cache) return json('no cache'); + + const existingUsers = await cache.get('meta', 'updatedBlentos'); const existingUsersArray: AppBskyActorDefs.ProfileViewDetailed[] = existingUsers ? JSON.parse(existingUsers) @@ -13,14 +15,12 @@ export async function GET({ platform }) { const existingUsersHandle = existingUsersArray.map((v) => v.handle); - const cache = platform?.env?.USER_DATA_CACHE as unknown; - for (const handle of existingUsersHandle) { if (!handle) continue; try { - const cached = await getCache(handle, 'self', cache as UserCache); - if (!cached) await loadData(handle, cache as UserCache, true); + const cached = await getCache(handle, 'self', cache); + if (!cached) await loadData(handle, cache, true); } catch (error) { console.error(error); return json('error'); diff --git a/src/routes/edit/+page.server.ts b/src/routes/edit/+page.server.ts index c571bf2..4eff4f3 100644 --- a/src/routes/edit/+page.server.ts +++ b/src/routes/edit/+page.server.ts @@ -1,6 +1,6 @@ import { loadData } from '$lib/website/load'; import { env } from '$env/dynamic/public'; -import type { UserCache } from '$lib/types'; +import { createCache } from '$lib/cache'; import type { ActorIdentifier } from '@atcute/lexicons'; export async function load({ platform, request }) { @@ -8,18 +8,18 @@ export async function load({ platform, request }) { const kv = platform?.env?.CUSTOM_DOMAINS; - const cache = platform?.env?.USER_DATA_CACHE as unknown; - const customDomain = request.headers.get('X-Custom-Domain')?.toLocaleLowerCase(); + const cache = createCache(platform); + const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); if (kv && customDomain) { try { const did = await kv.get(customDomain); - if (did) return await loadData(did as ActorIdentifier, cache as UserCache); + if (did) return await loadData(did as ActorIdentifier, cache); } catch (error) { console.error('failed to get custom domain kv', error); } } - return await loadData(handle as ActorIdentifier, cache as UserCache); + return await loadData(handle as ActorIdentifier, cache); } diff --git a/src/routes/p/[[page]]/+layout.server.ts b/src/routes/p/[[page]]/+layout.server.ts index faca3a0..8fbece5 100644 --- a/src/routes/p/[[page]]/+layout.server.ts +++ b/src/routes/p/[[page]]/+layout.server.ts @@ -1,10 +1,10 @@ import { loadData } from '$lib/website/load'; import { env } from '$env/dynamic/public'; -import type { UserCache } from '$lib/types'; +import { createCache } from '$lib/cache'; import type { Did, Handle } from '@atcute/lexicons'; export async function load({ params, platform, request }) { - const cache = platform?.env?.USER_DATA_CACHE as unknown; + const cache = createCache(platform); const handle = env.PUBLIC_HANDLE; @@ -15,11 +15,11 @@ export async function load({ params, platform, request }) { if (kv && customDomain) { try { const did = await kv.get(customDomain); - return await loadData(did as Did, cache as UserCache, false, params.page); + return await loadData(did as Did, cache, false, params.page); } catch { console.error('failed'); } } - return await loadData(handle as Handle, cache as UserCache, false, params.page); + return await loadData(handle as Handle, cache, false, params.page); } diff --git a/src/routes/random/+page.server.ts b/src/routes/random/+page.server.ts deleted file mode 100644 index e528e43..0000000 --- a/src/routes/random/+page.server.ts +++ /dev/null @@ -1,30 +0,0 @@ -import type { UserCache, WebsiteData } from '$lib/types.js'; -import { getCache } from '$lib/website/load.js'; -import { error } from '@sveltejs/kit'; - -export async function load({ platform }) { - const cache = platform?.env?.USER_DATA_CACHE; - - const list = await cache?.list(); - - if (!list) { - throw error(404); - } - - let foundData: WebsiteData | undefined = undefined; - let i = 0; - - while (!foundData && i < 20) { - const rando = Math.floor(Math.random() * list.keys.length); - console.log(list.keys[rando].name); - - foundData = await getCache(list.keys[rando].name, 'self', cache as unknown as UserCache); - - if (!foundData?.cards.length) foundData = undefined; - i++; - } - - if (!foundData) throw error(404); - - return foundData; -} diff --git a/src/routes/random/+page.svelte b/src/routes/random/+page.svelte deleted file mode 100644 index 98bab50..0000000 --- a/src/routes/random/+page.svelte +++ /dev/null @@ -1,40 +0,0 @@ - - - { - if (e.key === 'ArrowRight' || e.key === 'r') { - window.location.reload(); - } - }} -/> - - - - diff --git a/src/routes/test/domains/+server.ts b/src/routes/test/domains/+server.ts deleted file mode 100644 index f2ce628..0000000 --- a/src/routes/test/domains/+server.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { json } from '@sveltejs/kit'; - -export async function GET({ platform }) { - const kv = platform?.env?.CUSTOM_DOMAINS; - if (!kv) return json({ error: 'KV not available' }, { status: 500 }); - - const list = await kv.list(); - const entries: Record = {}; - - for (const key of list.keys) { - const value = await kv.get(key.name); - entries[key.name] = value ?? ''; - } - - return json(entries); -} -- 2.51.2 From 727abc306b449b02f9ee142a14ef364664e4be3e Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 8 Feb 2026 14:50:45 +0100 Subject: [PATCH 2/7] small fixes --- src/lib/cache.ts | 33 +++++++++++++++++------------- src/lib/layout/EditableGrid.svelte | 3 ++- src/lib/website/load.ts | 6 +++--- 3 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/lib/cache.ts b/src/lib/cache.ts index d509d7a..ba6f5fd 100644 --- a/src/lib/cache.ts +++ b/src/lib/cache.ts @@ -1,8 +1,10 @@ +import type { ActorIdentifier, Did } from '@atcute/lexicons'; +import { isDid } from '@atcute/lexicons/syntax'; import type { KVNamespace } from '@cloudflare/workers-types'; /** TTL in seconds for each cache namespace */ const NAMESPACE_TTL = { - user: 60 * 60 * 24, // 24 hours + blento: 60 * 60 * 24, // 24 hours identity: 60 * 60 * 24 * 7, // 7 days github: 60 * 60 * 12, // 12 hours 'gh-contrib': 60 * 60 * 12, // 12 hours @@ -22,7 +24,12 @@ export class CacheService { return this.kv.get(`${namespace}:${key}`); } - async put(namespace: CacheNamespace, key: string, value: string, ttlSeconds?: number): Promise { + async put( + namespace: CacheNamespace, + key: string, + value: string, + ttlSeconds?: number + ): Promise { const ttl = ttlSeconds ?? NAMESPACE_TTL[namespace] ?? 0; await this.kv.put(`${namespace}:${key}`, value, ttl > 0 ? { expirationTtl: ttl } : undefined); } @@ -54,34 +61,32 @@ export class CacheService { await this.put(namespace, key, JSON.stringify(value), ttlSeconds); } - // === User data (keyed by DID, with handle↔did resolution) === - - async getUser(identifier: string): Promise { + // === blento data (keyed by DID, with handle↔did resolution) === + async getBlento(identifier: ActorIdentifier): Promise { const did = await this.resolveDid(identifier); if (!did) return null; - return this.get('user', did); + return this.get('blento', did); } - async putUser(did: string, handle: string, data: string): Promise { + async putBlento(did: string, handle: string, data: string): Promise { await Promise.all([ - this.put('user', did, data), + this.put('blento', did, data), this.put('identity', `h:${handle}`, did), this.put('identity', `d:${did}`, handle) ]); } - async listUsers(): Promise { - return this.list('user'); + async listBlentos(): Promise { + return this.list('blento'); } // === Identity resolution === - - async resolveDid(identifier: string): Promise { - if (identifier.startsWith('did:')) return identifier; + async resolveDid(identifier: ActorIdentifier): Promise { + if (isDid(identifier)) return identifier; return this.get('identity', `h:${identifier}`); } - async resolveHandle(did: string): Promise { + async resolveHandle(did: Did): Promise { return this.get('identity', `d:${did}`); } } diff --git a/src/lib/layout/EditableGrid.svelte b/src/lib/layout/EditableGrid.svelte index 7ac9711..99522bf 100644 --- a/src/lib/layout/EditableGrid.svelte +++ b/src/lib/layout/EditableGrid.svelte @@ -373,7 +373,8 @@ ondrop={handleFileDrop} /> - + +
0) { const stringifiedResult = JSON.stringify(result); - await cache?.putUser(did, handle as string, stringifiedResult); + await cache?.putBlento(did, handle as string, stringifiedResult); } const parsedResult = structuredClone(result) as any; -- 2.51.2 From bfe09cf70a51f7ed32571802cbfa0ec687d8e9b2 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 8 Feb 2026 15:33:45 +0100 Subject: [PATCH 3/7] loadData -> loadDataServer --- .../LastFMCard/LastFMProfileCard/index.ts | 33 +++++++++++++++++ .../LastFMRecentTracksCard/index.ts | 33 +++++++++++++++++ .../LastFMCard/LastFMTopAlbumsCard/index.ts | 36 +++++++++++++++++++ .../LastFMCard/LastFMTopTracksCard/index.ts | 36 +++++++++++++++++++ .../social/GitHubContributorsCard/index.ts | 26 ++++++++++++++ .../cards/social/GitHubProfileCard/index.ts | 26 ++++++++++++++ .../social/NpmxLikesLeaderboardCard/index.ts | 16 +++++++++ src/lib/cards/types.ts | 17 +++++++++ src/lib/website/load.ts | 17 +++++---- src/routes/+page.server.ts | 5 +-- .../[actor=actor]/(pages)/+layout.server.ts | 2 +- .../site.standard.publication/+server.ts | 3 +- .../[actor=actor]/api/refresh/+server.ts | 3 +- src/routes/[actor=actor]/og.png/+server.ts | 3 +- src/routes/api/update/+server.ts | 3 +- src/routes/edit/+page.server.ts | 5 +-- src/routes/p/[[page]]/+layout.server.ts | 5 +-- svelte.config.js | 4 +++ 18 files changed, 256 insertions(+), 17 deletions(-) diff --git a/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts index c54efa6..f2cda96 100644 --- a/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts @@ -31,6 +31,39 @@ export const LastFMProfileCardDefinition = { } return allData; }, + loadDataServer: async (items, { cache, env }) => { + const apiKey = env?.LASTFM_API_KEY; + if (!apiKey) return {}; + const allData: Record = {}; + for (const item of items) { + const username = item.cardData.lastfmUsername; + if (!username) continue; + try { + const cacheKey = `user.getInfo:${username}:7day:50`; + const cached = await cache?.get('lastfm', cacheKey); + if (cached) { + allData[`lastfmProfile:${username}`] = JSON.parse(cached)?.user; + continue; + } + const params = new URLSearchParams({ + method: 'user.getInfo', + user: username, + api_key: apiKey, + format: 'json', + limit: '50' + }); + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); + if (!response.ok) continue; + const data = await response.json(); + if (data.error) continue; + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 12 * 60 * 60); + allData[`lastfmProfile:${username}`] = data?.user; + } catch (error) { + console.error('Failed to fetch Last.fm profile:', error); + } + } + return allData; + }, minW: 2, minH: 2, name: 'Last.fm Profile', diff --git a/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts index ec097cc..b722944 100644 --- a/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts @@ -31,6 +31,39 @@ export const LastFMRecentTracksCardDefinition = { } return allData; }, + loadDataServer: async (items, { cache, env }) => { + const apiKey = env?.LASTFM_API_KEY; + if (!apiKey) return {}; + const allData: Record = {}; + for (const item of items) { + const username = item.cardData.lastfmUsername; + if (!username) continue; + try { + const cacheKey = `user.getRecentTracks:${username}:7day:50`; + const cached = await cache?.get('lastfm', cacheKey); + if (cached) { + allData[`lastfmRecentTracks:${username}`] = JSON.parse(cached)?.recenttracks?.track ?? []; + continue; + } + const params = new URLSearchParams({ + method: 'user.getRecentTracks', + user: username, + api_key: apiKey, + format: 'json', + limit: '50' + }); + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); + if (!response.ok) continue; + const data = await response.json(); + if (data.error) continue; + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 15 * 60); + allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; + } catch (error) { + console.error('Failed to fetch Last.fm recent tracks:', error); + } + } + return allData; + }, onUrlHandler: (url, item) => { const username = getLastFMUsername(url); if (!username) return null; diff --git a/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts index 51de857..0a8ef2b 100644 --- a/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts @@ -35,6 +35,42 @@ export const LastFMTopAlbumsCardDefinition = { } return allData; }, + loadDataServer: async (items, { cache, env }) => { + const apiKey = env?.LASTFM_API_KEY; + if (!apiKey) return {}; + const allData: Record = {}; + for (const item of items) { + const username = item.cardData.lastfmUsername; + const period = item.cardData.period ?? '7day'; + if (!username) continue; + try { + const cacheKey = `user.getTopAlbums:${username}:${period}:50`; + const cached = await cache?.get('lastfm', cacheKey); + if (cached) { + allData[`lastfmTopAlbums:${username}:${period}`] = + JSON.parse(cached)?.topalbums?.album ?? []; + continue; + } + const params = new URLSearchParams({ + method: 'user.getTopAlbums', + user: username, + api_key: apiKey, + format: 'json', + limit: '50', + period + }); + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); + if (!response.ok) continue; + const data = await response.json(); + if (data.error) continue; + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 60 * 60); + allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; + } catch (error) { + console.error('Failed to fetch Last.fm top albums:', error); + } + } + return allData; + }, allowSetColor: true, defaultColor: 'base', minW: 2, diff --git a/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts index f7534bc..8d239d4 100644 --- a/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts @@ -35,6 +35,42 @@ export const LastFMTopTracksCardDefinition = { } return allData; }, + loadDataServer: async (items, { cache, env }) => { + const apiKey = env?.LASTFM_API_KEY; + if (!apiKey) return {}; + const allData: Record = {}; + for (const item of items) { + const username = item.cardData.lastfmUsername; + const period = item.cardData.period ?? '7day'; + if (!username) continue; + try { + const cacheKey = `user.getTopTracks:${username}:${period}:50`; + const cached = await cache?.get('lastfm', cacheKey); + if (cached) { + allData[`lastfmTopTracks:${username}:${period}`] = + JSON.parse(cached)?.toptracks?.track ?? []; + continue; + } + const params = new URLSearchParams({ + method: 'user.getTopTracks', + user: username, + api_key: apiKey, + format: 'json', + limit: '50', + period + }); + const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); + if (!response.ok) continue; + const data = await response.json(); + if (data.error) continue; + await cache?.put('lastfm', cacheKey, JSON.stringify(data), 60 * 60); + allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; + } catch (error) { + console.error('Failed to fetch Last.fm top tracks:', error); + } + } + return allData; + }, minW: 3, minH: 2, canHaveLabel: true, diff --git a/src/lib/cards/social/GitHubContributorsCard/index.ts b/src/lib/cards/social/GitHubContributorsCard/index.ts index 13938f3..7e3a9f9 100644 --- a/src/lib/cards/social/GitHubContributorsCard/index.ts +++ b/src/lib/cards/social/GitHubContributorsCard/index.ts @@ -45,6 +45,32 @@ export const GitHubContributorsCardDefinition = { } return contributorsData; }, + loadDataServer: async (items, { cache }) => { + const contributorsData: GitHubContributorsLoadedData = {}; + for (const item of items) { + const { owner, repo } = item.cardData; + if (!owner || !repo) continue; + const key = `${owner}/${repo}`; + if (contributorsData[key]) continue; + try { + const cached = await cache?.get('gh-contrib', key); + if (cached) { + contributorsData[key] = JSON.parse(cached); + continue; + } + const response = await fetch( + `https://edge-function-github-contribution.vercel.app/api/github-contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` + ); + if (!response.ok) continue; + const data = await response.json(); + await cache?.put('gh-contrib', key, JSON.stringify(data)); + contributorsData[key] = data; + } catch (error) { + console.error('Failed to fetch GitHub contributors:', error); + } + } + return contributorsData; + }, onUrlHandler: (url, item) => { const match = url.match(/github\.com\/([^/]+)\/([^/]+)/); if (!match) return null; diff --git a/src/lib/cards/social/GitHubProfileCard/index.ts b/src/lib/cards/social/GitHubProfileCard/index.ts index 2f793e5..3fc6b8f 100644 --- a/src/lib/cards/social/GitHubProfileCard/index.ts +++ b/src/lib/cards/social/GitHubProfileCard/index.ts @@ -27,6 +27,32 @@ export const GithubProfileCardDefitition = { } return githubData; }, + loadDataServer: async (items, { cache }) => { + const githubData: Record = {}; + for (const item of items) { + const user = item.cardData.user; + if (!user) continue; + try { + const cached = await cache?.get('github', user); + if (cached) { + githubData[user] = JSON.parse(cached); + continue; + } + const response = await fetch( + `https://edge-function-github-contribution.vercel.app/api/github-data?user=${encodeURIComponent(user)}` + ); + if (!response.ok) continue; + const data = await response.json(); + if (!data?.user) continue; + const result = data.user as GitHubContributionsData; + await cache?.put('github', user, JSON.stringify(result)); + githubData[user] = result; + } catch (error) { + console.error('Failed to fetch GitHub contributions:', error); + } + } + return githubData; + }, onUrlHandler: (url, item) => { const username = getGitHubUsername(url); diff --git a/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts b/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts index 167ab1f..faa1812 100644 --- a/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts +++ b/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts @@ -15,6 +15,22 @@ export const NpmxLikesLeaderboardCardDefinition = { const data = await res.json(); return data; }, + loadDataServer: async (_items, { cache }) => { + try { + const cached = await cache?.get('npmx', 'likes'); + if (cached) return JSON.parse(cached); + const response = await fetch( + 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20' + ); + if (!response.ok) return undefined; + const data = await response.json(); + await cache?.put('npmx', 'likes', JSON.stringify(data)); + return data; + } catch (error) { + console.error('Error fetching npmx leaderboard:', error); + return undefined; + } + }, minW: 3, canHaveLabel: true, diff --git a/src/lib/cards/types.ts b/src/lib/cards/types.ts index 995fa24..e4084de 100644 --- a/src/lib/cards/types.ts +++ b/src/lib/cards/types.ts @@ -40,6 +40,23 @@ export type CardDefinition = { { did, handle, cache }: { did: Did; handle: string; cache?: CacheService } ) => Promise; + // server-side version of loadData that calls external APIs directly + // instead of going through self-referential /api/ routes (avoids 522 on Cloudflare Workers) + loadDataServer?: ( + items: Item[], + { + did, + handle, + cache, + env + }: { + did: Did; + handle: string; + cache?: CacheService; + env?: Record; + } + ) => Promise; + // show color selection popup allowSetColor?: boolean; diff --git a/src/lib/website/load.ts b/src/lib/website/load.ts index 81b6460..b898971 100644 --- a/src/lib/website/load.ts +++ b/src/lib/website/load.ts @@ -44,7 +44,8 @@ export async function loadData( handle: ActorIdentifier, cache: CacheService | undefined, forceUpdate: boolean = false, - page: string = 'self' + page: string = 'self', + env?: Record ): Promise { if (!handle) throw error(404); if (handle === 'favicon.ico') throw error(404); @@ -94,13 +95,17 @@ export async function loadData( for (const cardType of cardTypesArray) { const cardDef = CardDefinitionsByType[cardType]; - if (!cardDef?.loadData) continue; + const items = cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[]; try { - additionDataPromises[cardType] = cardDef.loadData( - cards.filter((v) => cardType === v.value.cardType).map((v) => v.value) as Item[], - loadOptions - ); + if (cardDef?.loadDataServer) { + additionDataPromises[cardType] = cardDef.loadDataServer(items, { + ...loadOptions, + env + }); + } else if (cardDef?.loadData) { + additionDataPromises[cardType] = cardDef.loadData(items, loadOptions); + } } catch { console.error('error getting additional data for', cardType); } diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index add0f2c..c5c6996 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -1,5 +1,6 @@ import { loadData } from '$lib/website/load'; import { env } from '$env/dynamic/public'; +import { env as privateEnv } from '$env/dynamic/private'; import { createCache } from '$lib/cache'; import type { ActorIdentifier } from '@atcute/lexicons'; @@ -15,11 +16,11 @@ export async function load({ platform, request }) { try { const did = await kv.get(customDomain); - if (did) return await loadData(did as ActorIdentifier, cache); + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); } catch (error) { console.error('failed to get custom domain kv', error); } } - return await loadData(handle as ActorIdentifier, cache); + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); } diff --git a/src/routes/[actor=actor]/(pages)/+layout.server.ts b/src/routes/[actor=actor]/(pages)/+layout.server.ts index 79a5167..21a5695 100644 --- a/src/routes/[actor=actor]/(pages)/+layout.server.ts +++ b/src/routes/[actor=actor]/(pages)/+layout.server.ts @@ -9,5 +9,5 @@ export async function load({ params, platform }) { const cache = createCache(platform); - return await loadData(params.actor, cache, false, params.page); + return await loadData(params.actor, cache, false, params.page, env); } diff --git a/src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts b/src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts index 63bfa6e..a4275a7 100644 --- a/src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts +++ b/src/routes/[actor=actor]/.well-known/site.standard.publication/+server.ts @@ -1,5 +1,6 @@ import { loadData } from '$lib/website/load'; import { createCache } from '$lib/cache'; +import { env } from '$env/dynamic/private'; import { error } from '@sveltejs/kit'; import { text } from '@sveltejs/kit'; @@ -7,7 +8,7 @@ import { text } from '@sveltejs/kit'; export async function GET({ params, platform }) { const cache = createCache(platform); - const data = await loadData(params.actor, cache, false, params.page); + const data = await loadData(params.actor, cache, false, params.page, env); if (!data.publication) throw error(300); diff --git a/src/routes/[actor=actor]/api/refresh/+server.ts b/src/routes/[actor=actor]/api/refresh/+server.ts index 2a315fe..35e5d73 100644 --- a/src/routes/[actor=actor]/api/refresh/+server.ts +++ b/src/routes/[actor=actor]/api/refresh/+server.ts @@ -1,5 +1,6 @@ import { createCache } from '$lib/cache'; import { loadData } from '$lib/website/load.js'; +import { env } from '$env/dynamic/private'; import type { Handle } from '@atcute/lexicons'; import { json } from '@sveltejs/kit'; @@ -7,7 +8,7 @@ export async function GET({ params, platform }) { const cache = createCache(platform); if (!cache) return json('no cache'); - await loadData(params.actor, cache, true); + await loadData(params.actor, cache, true, 'self', env); return json('ok'); } diff --git a/src/routes/[actor=actor]/og.png/+server.ts b/src/routes/[actor=actor]/og.png/+server.ts index 122d800..c469fb3 100644 --- a/src/routes/[actor=actor]/og.png/+server.ts +++ b/src/routes/[actor=actor]/og.png/+server.ts @@ -1,6 +1,7 @@ import { getCDNImageBlobUrl } from '$lib/atproto/methods.js'; import { createCache } from '$lib/cache'; import { loadData } from '$lib/website/load'; +import { env } from '$env/dynamic/private'; import type { Handle } from '@atcute/lexicons'; import { ImageResponse } from '@ethercorps/sveltekit-og'; @@ -16,7 +17,7 @@ function escapeHtml(str: string): string { export async function GET({ params, platform }) { const cache = createCache(platform); - const data = await loadData(params.actor, cache); + const data = await loadData(params.actor, cache, false, 'self', env); let image: string | undefined = data.profile.avatar; diff --git a/src/routes/api/update/+server.ts b/src/routes/api/update/+server.ts index c35560a..0b4ad92 100644 --- a/src/routes/api/update/+server.ts +++ b/src/routes/api/update/+server.ts @@ -1,5 +1,6 @@ import { createCache } from '$lib/cache'; import { getCache, loadData } from '$lib/website/load'; +import { env } from '$env/dynamic/private'; import { json } from '@sveltejs/kit'; import type { AppBskyActorDefs } from '@atcute/bluesky'; @@ -20,7 +21,7 @@ export async function GET({ platform }) { try { const cached = await getCache(handle, 'self', cache); - if (!cached) await loadData(handle, cache, true); + if (!cached) await loadData(handle, cache, true, 'self', env); } catch (error) { console.error(error); return json('error'); diff --git a/src/routes/edit/+page.server.ts b/src/routes/edit/+page.server.ts index 4eff4f3..c61c78f 100644 --- a/src/routes/edit/+page.server.ts +++ b/src/routes/edit/+page.server.ts @@ -1,5 +1,6 @@ import { loadData } from '$lib/website/load'; import { env } from '$env/dynamic/public'; +import { env as privateEnv } from '$env/dynamic/private'; import { createCache } from '$lib/cache'; import type { ActorIdentifier } from '@atcute/lexicons'; @@ -15,11 +16,11 @@ export async function load({ platform, request }) { try { const did = await kv.get(customDomain); - if (did) return await loadData(did as ActorIdentifier, cache); + if (did) return await loadData(did as ActorIdentifier, cache, false, 'self', privateEnv); } catch (error) { console.error('failed to get custom domain kv', error); } } - return await loadData(handle as ActorIdentifier, cache); + return await loadData(handle as ActorIdentifier, cache, false, 'self', privateEnv); } diff --git a/src/routes/p/[[page]]/+layout.server.ts b/src/routes/p/[[page]]/+layout.server.ts index 8fbece5..f320e90 100644 --- a/src/routes/p/[[page]]/+layout.server.ts +++ b/src/routes/p/[[page]]/+layout.server.ts @@ -1,5 +1,6 @@ import { loadData } from '$lib/website/load'; import { env } from '$env/dynamic/public'; +import { env as privateEnv } from '$env/dynamic/private'; import { createCache } from '$lib/cache'; import type { Did, Handle } from '@atcute/lexicons'; @@ -15,11 +16,11 @@ export async function load({ params, platform, request }) { if (kv && customDomain) { try { const did = await kv.get(customDomain); - return await loadData(did as Did, cache, false, params.page); + return await loadData(did as Did, cache, false, params.page, privateEnv); } catch { console.error('failed'); } } - return await loadData(handle as Handle, cache, false, params.page); + return await loadData(handle as Handle, cache, false, params.page, privateEnv); } diff --git a/svelte.config.js b/svelte.config.js index 309550b..eabf69c 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -11,8 +11,12 @@ const config = { adapter: adapter(), paths: { base: '' + }, + experimental: { + remoteFunctions: true } }, + compilerOptions: { experimental: { async: true -- 2.51.2 From 579fb3c2b31bd784d0c30b14944746b8973222dd Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 8 Feb 2026 15:57:41 +0100 Subject: [PATCH 4/7] move to remote functions --- .../LastFMCard/LastFMProfileCard/index.ts | 35 ++--------- .../LastFMRecentTracksCard/index.ts | 35 ++--------- .../LastFMCard/LastFMTopAlbumsCard/index.ts | 37 ++---------- .../LastFMCard/LastFMTopTracksCard/index.ts | 37 ++---------- src/lib/cards/media/LastFMCard/api.remote.ts | 59 +++++++++++++++++++ .../GitHubContributorsCard/api.remote.ts | 26 ++++++++ .../social/GitHubContributorsCard/index.ts | 25 ++------ .../social/GitHubProfileCard/api.remote.ts | 21 +++++++ .../cards/social/GitHubProfileCard/index.ts | 32 +++------- .../NpmxLikesLeaderboardCard/api.remote.ts | 20 +++++++ .../social/NpmxLikesLeaderboardCard/index.ts | 22 ++----- 11 files changed, 169 insertions(+), 180 deletions(-) create mode 100644 src/lib/cards/media/LastFMCard/api.remote.ts create mode 100644 src/lib/cards/social/GitHubContributorsCard/api.remote.ts create mode 100644 src/lib/cards/social/GitHubProfileCard/api.remote.ts create mode 100644 src/lib/cards/social/NpmxLikesLeaderboardCard/api.remote.ts diff --git a/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts index f2cda96..00b128d 100644 --- a/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMProfileCard/index.ts @@ -1,6 +1,7 @@ import type { CardDefinition } from '../../../types'; import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; import LastFMProfileCard from './LastFMProfileCard.svelte'; +import { fetchLastFM } from '../api.remote'; export const LastFMProfileCardDefinition = { type: 'lastfmProfile', @@ -18,46 +19,22 @@ export const LastFMProfileCardDefinition = { const username = item.cardData.lastfmUsername; if (!username) continue; try { - const response = await fetch( - `https://blento.app/api/lastfm?method=user.getInfo&user=${encodeURIComponent(username)}` - ); - if (!response.ok) continue; - const text = await response.text(); - const result = JSON.parse(text); - allData[`lastfmProfile:${username}`] = result?.user; + const data = await fetchLastFM({ method: 'user.getInfo', user: username }); + if (data) allData[`lastfmProfile:${username}`] = data?.user; } catch (error) { console.error('Failed to fetch Last.fm profile:', error); } } return allData; }, - loadDataServer: async (items, { cache, env }) => { - const apiKey = env?.LASTFM_API_KEY; - if (!apiKey) return {}; + loadDataServer: async (items) => { const allData: Record = {}; for (const item of items) { const username = item.cardData.lastfmUsername; if (!username) continue; try { - const cacheKey = `user.getInfo:${username}:7day:50`; - const cached = await cache?.get('lastfm', cacheKey); - if (cached) { - allData[`lastfmProfile:${username}`] = JSON.parse(cached)?.user; - continue; - } - const params = new URLSearchParams({ - method: 'user.getInfo', - user: username, - api_key: apiKey, - format: 'json', - limit: '50' - }); - const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); - if (!response.ok) continue; - const data = await response.json(); - if (data.error) continue; - await cache?.put('lastfm', cacheKey, JSON.stringify(data), 12 * 60 * 60); - allData[`lastfmProfile:${username}`] = data?.user; + const data = await fetchLastFM({ method: 'user.getInfo', user: username }); + if (data) allData[`lastfmProfile:${username}`] = data?.user; } catch (error) { console.error('Failed to fetch Last.fm profile:', error); } diff --git a/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts index b722944..26329c4 100644 --- a/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/index.ts @@ -1,6 +1,7 @@ import type { CardDefinition } from '../../../types'; import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; import LastFMRecentTracksCard from './LastFMRecentTracksCard.svelte'; +import { fetchLastFM } from '../api.remote'; export const LastFMRecentTracksCardDefinition = { type: 'lastfmRecentTracks', @@ -18,46 +19,22 @@ export const LastFMRecentTracksCardDefinition = { const username = item.cardData.lastfmUsername; if (!username) continue; try { - const response = await fetch( - `https://blento.app/api/lastfm?method=user.getRecentTracks&user=${encodeURIComponent(username)}&limit=50` - ); - if (!response.ok) continue; - const text = await response.text(); - const result = JSON.parse(text); - allData[`lastfmRecentTracks:${username}`] = result?.recenttracks?.track ?? []; + const data = await fetchLastFM({ method: 'user.getRecentTracks', user: username }); + if (data) allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; } catch (error) { console.error('Failed to fetch Last.fm recent tracks:', error); } } return allData; }, - loadDataServer: async (items, { cache, env }) => { - const apiKey = env?.LASTFM_API_KEY; - if (!apiKey) return {}; + loadDataServer: async (items) => { const allData: Record = {}; for (const item of items) { const username = item.cardData.lastfmUsername; if (!username) continue; try { - const cacheKey = `user.getRecentTracks:${username}:7day:50`; - const cached = await cache?.get('lastfm', cacheKey); - if (cached) { - allData[`lastfmRecentTracks:${username}`] = JSON.parse(cached)?.recenttracks?.track ?? []; - continue; - } - const params = new URLSearchParams({ - method: 'user.getRecentTracks', - user: username, - api_key: apiKey, - format: 'json', - limit: '50' - }); - const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); - if (!response.ok) continue; - const data = await response.json(); - if (data.error) continue; - await cache?.put('lastfm', cacheKey, JSON.stringify(data), 15 * 60); - allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; + const data = await fetchLastFM({ method: 'user.getRecentTracks', user: username }); + if (data) allData[`lastfmRecentTracks:${username}`] = data?.recenttracks?.track ?? []; } catch (error) { console.error('Failed to fetch Last.fm recent tracks:', error); } diff --git a/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts index 0a8ef2b..1c61222 100644 --- a/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/index.ts @@ -2,6 +2,7 @@ import type { CardDefinition } from '../../../types'; import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; import LastFMTopAlbumsCard from './LastFMTopAlbumsCard.svelte'; import LastFMTopAlbumsCardSettings from './LastFMTopAlbumsCardSettings.svelte'; +import { fetchLastFM } from '../api.remote'; export const LastFMTopAlbumsCardDefinition = { type: 'lastfmTopAlbums', @@ -22,49 +23,23 @@ export const LastFMTopAlbumsCardDefinition = { const period = item.cardData.period ?? '7day'; if (!username) continue; try { - const response = await fetch( - `https://blento.app/api/lastfm?method=user.getTopAlbums&user=${encodeURIComponent(username)}&period=${period}&limit=50` - ); - if (!response.ok) continue; - const text = await response.text(); - const result = JSON.parse(text); - allData[`lastfmTopAlbums:${username}:${period}`] = result?.topalbums?.album ?? []; + const data = await fetchLastFM({ method: 'user.getTopAlbums', user: username, period }); + if (data) allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; } catch (error) { console.error('Failed to fetch Last.fm top albums:', error); } } return allData; }, - loadDataServer: async (items, { cache, env }) => { - const apiKey = env?.LASTFM_API_KEY; - if (!apiKey) return {}; + loadDataServer: async (items) => { const allData: Record = {}; for (const item of items) { const username = item.cardData.lastfmUsername; const period = item.cardData.period ?? '7day'; if (!username) continue; try { - const cacheKey = `user.getTopAlbums:${username}:${period}:50`; - const cached = await cache?.get('lastfm', cacheKey); - if (cached) { - allData[`lastfmTopAlbums:${username}:${period}`] = - JSON.parse(cached)?.topalbums?.album ?? []; - continue; - } - const params = new URLSearchParams({ - method: 'user.getTopAlbums', - user: username, - api_key: apiKey, - format: 'json', - limit: '50', - period - }); - const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); - if (!response.ok) continue; - const data = await response.json(); - if (data.error) continue; - await cache?.put('lastfm', cacheKey, JSON.stringify(data), 60 * 60); - allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; + const data = await fetchLastFM({ method: 'user.getTopAlbums', user: username, period }); + if (data) allData[`lastfmTopAlbums:${username}:${period}`] = data?.topalbums?.album ?? []; } catch (error) { console.error('Failed to fetch Last.fm top albums:', error); } diff --git a/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts b/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts index 8d239d4..eab1322 100644 --- a/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts +++ b/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/index.ts @@ -2,6 +2,7 @@ import type { CardDefinition } from '../../../types'; import CreateLastFMCardModal from '../CreateLastFMCardModal.svelte'; import LastFMPeriodSettings from '../LastFMPeriodSettings.svelte'; import LastFMTopTracksCard from './LastFMTopTracksCard.svelte'; +import { fetchLastFM } from '../api.remote'; export const LastFMTopTracksCardDefinition = { type: 'lastfmTopTracks', @@ -22,49 +23,23 @@ export const LastFMTopTracksCardDefinition = { const period = item.cardData.period ?? '7day'; if (!username) continue; try { - const response = await fetch( - `https://blento.app/api/lastfm?method=user.getTopTracks&user=${encodeURIComponent(username)}&period=${period}&limit=50` - ); - if (!response.ok) continue; - const text = await response.text(); - const result = JSON.parse(text); - allData[`lastfmTopTracks:${username}:${period}`] = result?.toptracks?.track ?? []; + const data = await fetchLastFM({ method: 'user.getTopTracks', user: username, period }); + if (data) allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; } catch (error) { console.error('Failed to fetch Last.fm top tracks:', error); } } return allData; }, - loadDataServer: async (items, { cache, env }) => { - const apiKey = env?.LASTFM_API_KEY; - if (!apiKey) return {}; + loadDataServer: async (items) => { const allData: Record = {}; for (const item of items) { const username = item.cardData.lastfmUsername; const period = item.cardData.period ?? '7day'; if (!username) continue; try { - const cacheKey = `user.getTopTracks:${username}:${period}:50`; - const cached = await cache?.get('lastfm', cacheKey); - if (cached) { - allData[`lastfmTopTracks:${username}:${period}`] = - JSON.parse(cached)?.toptracks?.track ?? []; - continue; - } - const params = new URLSearchParams({ - method: 'user.getTopTracks', - user: username, - api_key: apiKey, - format: 'json', - limit: '50', - period - }); - const response = await fetch(`https://ws.audioscrobbler.com/2.0/?${params}`); - if (!response.ok) continue; - const data = await response.json(); - if (data.error) continue; - await cache?.put('lastfm', cacheKey, JSON.stringify(data), 60 * 60); - allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; + const data = await fetchLastFM({ method: 'user.getTopTracks', user: username, period }); + if (data) allData[`lastfmTopTracks:${username}:${period}`] = data?.toptracks?.track ?? []; } catch (error) { console.error('Failed to fetch Last.fm top tracks:', error); } diff --git a/src/lib/cards/media/LastFMCard/api.remote.ts b/src/lib/cards/media/LastFMCard/api.remote.ts new file mode 100644 index 0000000..25c08bd --- /dev/null +++ b/src/lib/cards/media/LastFMCard/api.remote.ts @@ -0,0 +1,59 @@ +import { query, getRequestEvent } from '$app/server'; +import { env } from '$env/dynamic/private'; +import { createCache } from '$lib/cache'; + +const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; + +const CACHE_TTL: Record = { + 'user.getRecentTracks': 15 * 60, + 'user.getTopTracks': 60 * 60, + 'user.getTopAlbums': 60 * 60, + 'user.getInfo': 12 * 60 * 60 +}; + +export const fetchLastFM = query( + 'unchecked', + async ({ + method, + user, + period = '7day', + limit = '50' + }: { + method: string; + user: string; + period?: string; + limit?: string; + }) => { + const apiKey = env?.LASTFM_API_KEY; + if (!apiKey) return undefined; + + const { platform } = getRequestEvent(); + const cache = createCache(platform); + + const cacheKey = `${method}:${user}:${period}:${limit}`; + const cached = await cache?.get('lastfm', cacheKey); + if (cached) return JSON.parse(cached); + + const params = new URLSearchParams({ + method, + user, + api_key: apiKey, + format: 'json', + limit + }); + + if (method === 'user.getTopTracks' || method === 'user.getTopAlbums') { + params.set('period', period); + } + + const response = await fetch(`${LASTFM_API_URL}?${params}`); + if (!response.ok) return undefined; + + const data = await response.json(); + if (data.error) return undefined; + + const ttl = CACHE_TTL[method] || 60 * 60; + await cache?.put('lastfm', cacheKey, JSON.stringify(data), ttl); + return data; + } +); diff --git a/src/lib/cards/social/GitHubContributorsCard/api.remote.ts b/src/lib/cards/social/GitHubContributorsCard/api.remote.ts new file mode 100644 index 0000000..61db84d --- /dev/null +++ b/src/lib/cards/social/GitHubContributorsCard/api.remote.ts @@ -0,0 +1,26 @@ +import { query, getRequestEvent } from '$app/server'; +import { createCache } from '$lib/cache'; + +const GITHUB_CONTRIBUTORS_API_URL = + 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; + +export const fetchGitHubContributors = query( + 'unchecked', + async ({ owner, repo }: { owner: string; repo: string }) => { + const { platform } = getRequestEvent(); + const cache = createCache(platform); + + const key = `${owner}/${repo}`; + const cached = await cache?.get('gh-contrib', key); + if (cached) return JSON.parse(cached); + + const response = await fetch( + `${GITHUB_CONTRIBUTORS_API_URL}?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` + ); + if (!response.ok) return undefined; + + const data = await response.json(); + await cache?.put('gh-contrib', key, JSON.stringify(data)); + return data; + } +); diff --git a/src/lib/cards/social/GitHubContributorsCard/index.ts b/src/lib/cards/social/GitHubContributorsCard/index.ts index 7e3a9f9..cb65d6a 100644 --- a/src/lib/cards/social/GitHubContributorsCard/index.ts +++ b/src/lib/cards/social/GitHubContributorsCard/index.ts @@ -2,6 +2,7 @@ import type { CardDefinition } from '../../types'; import GitHubContributorsCard from './GitHubContributorsCard.svelte'; import CreateGitHubContributorsCardModal from './CreateGitHubContributorsCardModal.svelte'; import GitHubContributorsCardSettings from './GitHubContributorsCardSettings.svelte'; +import { fetchGitHubContributors } from './api.remote'; export type GitHubContributor = { username: string; @@ -33,19 +34,15 @@ export const GitHubContributorsCardDefinition = { const key = `${owner}/${repo}`; if (contributorsData[key]) continue; try { - const response = await fetch( - `https://blento.app/api/github/contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` - ); - if (response.ok) { - contributorsData[key] = await response.json(); - } + const data = await fetchGitHubContributors({ owner, repo }); + if (data) contributorsData[key] = data; } catch (error) { console.error('Failed to fetch GitHub contributors:', error); } } return contributorsData; }, - loadDataServer: async (items, { cache }) => { + loadDataServer: async (items) => { const contributorsData: GitHubContributorsLoadedData = {}; for (const item of items) { const { owner, repo } = item.cardData; @@ -53,18 +50,8 @@ export const GitHubContributorsCardDefinition = { const key = `${owner}/${repo}`; if (contributorsData[key]) continue; try { - const cached = await cache?.get('gh-contrib', key); - if (cached) { - contributorsData[key] = JSON.parse(cached); - continue; - } - const response = await fetch( - `https://edge-function-github-contribution.vercel.app/api/github-contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` - ); - if (!response.ok) continue; - const data = await response.json(); - await cache?.put('gh-contrib', key, JSON.stringify(data)); - contributorsData[key] = data; + const data = await fetchGitHubContributors({ owner, repo }); + if (data) contributorsData[key] = data; } catch (error) { console.error('Failed to fetch GitHub contributors:', error); } diff --git a/src/lib/cards/social/GitHubProfileCard/api.remote.ts b/src/lib/cards/social/GitHubProfileCard/api.remote.ts new file mode 100644 index 0000000..28d0772 --- /dev/null +++ b/src/lib/cards/social/GitHubProfileCard/api.remote.ts @@ -0,0 +1,21 @@ +import { query, getRequestEvent } from '$app/server'; +import { createCache } from '$lib/cache'; + +const GITHUB_API_URL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; + +export const fetchGitHubContributions = query('unchecked', async (user: string) => { + const { platform } = getRequestEvent(); + const cache = createCache(platform); + + const cached = await cache?.get('github', user); + if (cached) return JSON.parse(cached); + + const response = await fetch(GITHUB_API_URL + encodeURIComponent(user)); + if (!response.ok) return undefined; + + const data = await response.json(); + if (!data?.user) return undefined; + + await cache?.put('github', user, JSON.stringify(data.user)); + return data.user; +}); diff --git a/src/lib/cards/social/GitHubProfileCard/index.ts b/src/lib/cards/social/GitHubProfileCard/index.ts index 3fc6b8f..6d29b54 100644 --- a/src/lib/cards/social/GitHubProfileCard/index.ts +++ b/src/lib/cards/social/GitHubProfileCard/index.ts @@ -1,8 +1,8 @@ import type { CardDefinition } from '../../types'; import CreateGitHubProfileCardModal from './CreateGitHubProfileCardModal.svelte'; -import type GithubContributionsGraph from './GithubContributionsGraph.svelte'; import GitHubProfileCard from './GitHubProfileCard.svelte'; import type { GitHubContributionsData } from './types'; +import { fetchGitHubContributions } from './api.remote'; export type GithubProfileLoadedData = Record; @@ -12,41 +12,27 @@ export const GithubProfileCardDefitition = { creationModalComponent: CreateGitHubProfileCardModal, loadData: async (items) => { - const githubData: Record = {}; + const githubData: Record = {}; for (const item of items) { + const user = item.cardData.user; + if (!user) continue; try { - const response = await fetch( - `https://blento.app/api/github?user=${encodeURIComponent(item.cardData.user)}` - ); - if (response.ok) { - githubData[item.cardData.user] = await response.json(); - } + const data = await fetchGitHubContributions(user); + if (data) githubData[user] = data; } catch (error) { console.error('Failed to fetch GitHub contributions:', error); } } return githubData; }, - loadDataServer: async (items, { cache }) => { + loadDataServer: async (items) => { const githubData: Record = {}; for (const item of items) { const user = item.cardData.user; if (!user) continue; try { - const cached = await cache?.get('github', user); - if (cached) { - githubData[user] = JSON.parse(cached); - continue; - } - const response = await fetch( - `https://edge-function-github-contribution.vercel.app/api/github-data?user=${encodeURIComponent(user)}` - ); - if (!response.ok) continue; - const data = await response.json(); - if (!data?.user) continue; - const result = data.user as GitHubContributionsData; - await cache?.put('github', user, JSON.stringify(result)); - githubData[user] = result; + const data = await fetchGitHubContributions(user); + if (data) githubData[user] = data; } catch (error) { console.error('Failed to fetch GitHub contributions:', error); } diff --git a/src/lib/cards/social/NpmxLikesLeaderboardCard/api.remote.ts b/src/lib/cards/social/NpmxLikesLeaderboardCard/api.remote.ts new file mode 100644 index 0000000..14a8507 --- /dev/null +++ b/src/lib/cards/social/NpmxLikesLeaderboardCard/api.remote.ts @@ -0,0 +1,20 @@ +import { query, getRequestEvent } from '$app/server'; +import { createCache } from '$lib/cache'; + +const LEADERBOARD_API_URL = + 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; + +export const fetchNpmxLeaderboard = query(async () => { + const { platform } = getRequestEvent(); + const cache = createCache(platform); + + const cached = await cache?.get('npmx', 'likes'); + if (cached) return JSON.parse(cached); + + const response = await fetch(LEADERBOARD_API_URL); + if (!response.ok) return undefined; + + const data = await response.json(); + await cache?.put('npmx', 'likes', JSON.stringify(data)); + return data; +}); diff --git a/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts b/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts index faa1812..eef5e30 100644 --- a/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts +++ b/src/lib/cards/social/NpmxLikesLeaderboardCard/index.ts @@ -1,5 +1,6 @@ import type { CardDefinition } from '../../types'; import NpmxLikesLeaderboardCard from './NpmxLikesLeaderboardCard.svelte'; +import { fetchNpmxLeaderboard } from './api.remote'; export const NpmxLikesLeaderboardCardDefinition = { type: 'npmxLikesLeaderboard', @@ -11,25 +12,10 @@ export const NpmxLikesLeaderboardCardDefinition = { card.mobileH = 6; }, loadData: async () => { - const res = await fetch('https://blento.app/api/npmx-leaderboard'); - const data = await res.json(); - return data; + return await fetchNpmxLeaderboard(); }, - loadDataServer: async (_items, { cache }) => { - try { - const cached = await cache?.get('npmx', 'likes'); - if (cached) return JSON.parse(cached); - const response = await fetch( - 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20' - ); - if (!response.ok) return undefined; - const data = await response.json(); - await cache?.put('npmx', 'likes', JSON.stringify(data)); - return data; - } catch (error) { - console.error('Error fetching npmx leaderboard:', error); - return undefined; - } + loadDataServer: async () => { + return await fetchNpmxLeaderboard(); }, minW: 3, canHaveLabel: true, -- 2.51.2 From 7bcda16d745a052958c1869ae836891b4e3555df Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 8 Feb 2026 15:59:32 +0100 Subject: [PATCH 5/7] remove some logs --- src/routes/+layout.server.ts | 2 -- src/routes/+layout.svelte | 1 - 2 files changed, 3 deletions(-) diff --git a/src/routes/+layout.server.ts b/src/routes/+layout.server.ts index d148b9e..c7e11d2 100644 --- a/src/routes/+layout.server.ts +++ b/src/routes/+layout.server.ts @@ -1,7 +1,5 @@ export async function load({ request }) { const customDomain = request.headers.get('X-Custom-Domain')?.toLowerCase(); - console.log('domain:', customDomain); - return { customDomain }; } diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index 4153bfe..5a9eff7 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -17,7 +17,6 @@ }; onMount(() => { - console.log(data.customDomain); initClient({ customDomain: data.customDomain }); const error = page.url.searchParams.get('error'); -- 2.51.2 From cb5f312731eecb0115b4f3607bf85625e262c8a4 Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 8 Feb 2026 16:25:51 +0100 Subject: [PATCH 6/7] delete api routes --- .../LastFMProfileCard.svelte | 11 +-- .../LastFMRecentTracksCard.svelte | 11 +-- .../LastFMTopAlbumsCard.svelte | 12 +-- .../LastFMTopTracksCard.svelte | 12 +-- .../GitHubContributorsCard.svelte | 10 +-- .../GitHubProfileCard.svelte | 6 +- src/routes/api/github/+server.ts | 48 ----------- src/routes/api/github/contributors/+server.ts | 45 ---------- src/routes/api/lastfm/+server.ts | 85 ------------------- src/routes/api/npmx-leaderboard/+server.ts | 35 -------- 10 files changed, 32 insertions(+), 243 deletions(-) delete mode 100644 src/routes/api/github/+server.ts delete mode 100644 src/routes/api/github/contributors/+server.ts delete mode 100644 src/routes/api/lastfm/+server.ts delete mode 100644 src/routes/api/npmx-leaderboard/+server.ts diff --git a/src/lib/cards/media/LastFMCard/LastFMProfileCard/LastFMProfileCard.svelte b/src/lib/cards/media/LastFMCard/LastFMProfileCard/LastFMProfileCard.svelte index c1fa2e1..efbe586 100644 --- a/src/lib/cards/media/LastFMCard/LastFMProfileCard/LastFMProfileCard.svelte +++ b/src/lib/cards/media/LastFMCard/LastFMProfileCard/LastFMProfileCard.svelte @@ -4,6 +4,7 @@ import { getAdditionalUserData } from '$lib/website/context'; import type { ContentComponentProps } from '../../../types'; import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; + import { fetchLastFM } from '../api.remote'; interface UserInfo { name: string; @@ -27,11 +28,11 @@ if (!item.cardData.lastfmUsername) return; try { - const response = await fetch( - `/api/lastfm?method=user.getInfo&user=${encodeURIComponent(item.cardData.lastfmUsername)}` - ); - if (response.ok) { - const result = await response.json(); + const result = await fetchLastFM({ + method: 'user.getInfo', + user: item.cardData.lastfmUsername + }); + if (result) { userInfo = result?.user; data[cacheKey] = userInfo; } diff --git a/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/LastFMRecentTracksCard.svelte b/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/LastFMRecentTracksCard.svelte index 8f99b86..8511260 100644 --- a/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/LastFMRecentTracksCard.svelte +++ b/src/lib/cards/media/LastFMCard/LastFMRecentTracksCard/LastFMRecentTracksCard.svelte @@ -4,6 +4,7 @@ import type { ContentComponentProps } from '../../../types'; import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; import { RelativeTime } from '@foxui/time'; + import { fetchLastFM } from '../api.remote'; interface Track { name: string; @@ -29,11 +30,11 @@ if (!item.cardData.lastfmUsername) return; try { - const response = await fetch( - `/api/lastfm?method=user.getRecentTracks&user=${encodeURIComponent(item.cardData.lastfmUsername)}&limit=50` - ); - if (response.ok) { - const result = await response.json(); + const result = await fetchLastFM({ + method: 'user.getRecentTracks', + user: item.cardData.lastfmUsername + }); + if (result) { tracks = result?.recenttracks?.track ?? []; data[cacheKey] = tracks; } else { diff --git a/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/LastFMTopAlbumsCard.svelte b/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/LastFMTopAlbumsCard.svelte index ad09996..9200a11 100644 --- a/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/LastFMTopAlbumsCard.svelte +++ b/src/lib/cards/media/LastFMCard/LastFMTopAlbumsCard/LastFMTopAlbumsCard.svelte @@ -3,6 +3,7 @@ import type { ContentComponentProps } from '../../../types'; import { getAdditionalUserData } from '$lib/website/context'; import ImageGrid from '$lib/components/ImageGrid.svelte'; + import { fetchLastFM } from '../api.remote'; interface Album { name: string; @@ -30,11 +31,12 @@ loading = true; try { - const response = await fetch( - `/api/lastfm?method=user.getTopAlbums&user=${encodeURIComponent(item.cardData.lastfmUsername)}&period=${period}&limit=50` - ); - if (response.ok) { - const result = await response.json(); + const result = await fetchLastFM({ + method: 'user.getTopAlbums', + user: item.cardData.lastfmUsername, + period + }); + if (result) { albums = result?.topalbums?.album ?? []; data[cacheKey] = albums; } else { diff --git a/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/LastFMTopTracksCard.svelte b/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/LastFMTopTracksCard.svelte index 52320a9..c84c462 100644 --- a/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/LastFMTopTracksCard.svelte +++ b/src/lib/cards/media/LastFMCard/LastFMTopTracksCard/LastFMTopTracksCard.svelte @@ -3,6 +3,7 @@ import { getAdditionalUserData } from '$lib/website/context'; import type { ContentComponentProps } from '../../../types'; import LastFMAlbumArt from '../LastFMAlbumArt.svelte'; + import { fetchLastFM } from '../api.remote'; interface Track { name: string; @@ -29,11 +30,12 @@ loading = true; try { - const response = await fetch( - `/api/lastfm?method=user.getTopTracks&user=${encodeURIComponent(item.cardData.lastfmUsername)}&period=${period}&limit=50` - ); - if (response.ok) { - const result = await response.json(); + const result = await fetchLastFM({ + method: 'user.getTopTracks', + user: item.cardData.lastfmUsername, + period + }); + if (result) { tracks = result?.toptracks?.track ?? []; data[cacheKey] = tracks; } else { diff --git a/src/lib/cards/social/GitHubContributorsCard/GitHubContributorsCard.svelte b/src/lib/cards/social/GitHubContributorsCard/GitHubContributorsCard.svelte index 68b6d31..db8f9ff 100644 --- a/src/lib/cards/social/GitHubContributorsCard/GitHubContributorsCard.svelte +++ b/src/lib/cards/social/GitHubContributorsCard/GitHubContributorsCard.svelte @@ -4,6 +4,7 @@ import { getAdditionalUserData, getCanEdit } from '$lib/website/context'; import type { GitHubContributor, GitHubContributorsLoadedData } from '.'; import ImageGrid from '$lib/components/ImageGrid.svelte'; + import { fetchGitHubContributors } from './api.remote'; let { item }: ContentComponentProps = $props(); @@ -41,13 +42,8 @@ async function loadContributors() { if (!owner || !repo) return; try { - const response = await fetch( - `/api/github/contributors?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` - ); - if (response.ok) { - const data = await response.json(); - clientContributors = data; - } + const data = await fetchGitHubContributors({ owner, repo }); + if (data) clientContributors = data; } catch (error) { console.error('Failed to fetch GitHub contributors:', error); } diff --git a/src/lib/cards/social/GitHubProfileCard/GitHubProfileCard.svelte b/src/lib/cards/social/GitHubProfileCard/GitHubProfileCard.svelte index 3a351a1..470201e 100644 --- a/src/lib/cards/social/GitHubProfileCard/GitHubProfileCard.svelte +++ b/src/lib/cards/social/GitHubProfileCard/GitHubProfileCard.svelte @@ -8,6 +8,7 @@ import { Button } from '@foxui/core'; import { browser } from '$app/environment'; import { qrOverlay } from '$lib/components/qr/qrOverlay.svelte'; + import { fetchGitHubContributions } from './api.remote'; let { item, isEditing }: ContentComponentProps = $props(); @@ -23,9 +24,8 @@ onMount(async () => { if (!contributionsData && item.cardData?.user) { try { - const response = await fetch(`/api/github?user=${encodeURIComponent(item.cardData.user)}`); - if (response.ok) { - contributionsData = await response.json(); + contributionsData = await fetchGitHubContributions(item.cardData.user); + if (contributionsData) { data[item.cardType] ??= {}; (data[item.cardType] as GithubProfileLoadedData)[item.cardData.user] = contributionsData; } diff --git a/src/routes/api/github/+server.ts b/src/routes/api/github/+server.ts deleted file mode 100644 index 1bcb6dc..0000000 --- a/src/routes/api/github/+server.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import type { GitHubContributionsData } from '$lib/cards/social/GitHubProfileCard/types'; -import { createCache } from '$lib/cache'; - -const GithubAPIURL = 'https://edge-function-github-contribution.vercel.app/api/github-data?user='; - -export const GET: RequestHandler = async ({ url, platform }) => { - const user = url.searchParams.get('user'); - - if (!user) { - return json({ error: 'No user provided' }, { status: 400 }); - } - - const cache = createCache(platform); - const cachedData = await cache?.get('github', user); - - if (cachedData) { - return json(JSON.parse(cachedData)); - } - - try { - const response = await fetch(GithubAPIURL + user); - - if (!response.ok) { - console.error('error', response.statusText); - return json( - { error: 'Failed to fetch GitHub data ' + response.statusText }, - { status: response.status } - ); - } - - const data = await response.json(); - - if (!data?.user) { - return json({ error: 'User not found' }, { status: 404 }); - } - - const result = data.user as GitHubContributionsData; - - await cache?.put('github', user, JSON.stringify(result)); - - return json(result); - } catch (error) { - console.error('Error fetching GitHub contributions:', error); - return json({ error: 'Failed to fetch GitHub data' }, { status: 500 }); - } -}; diff --git a/src/routes/api/github/contributors/+server.ts b/src/routes/api/github/contributors/+server.ts deleted file mode 100644 index 7f28736..0000000 --- a/src/routes/api/github/contributors/+server.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { createCache } from '$lib/cache'; - -const GithubContributorsAPIURL = - 'https://edge-function-github-contribution.vercel.app/api/github-contributors'; - -export const GET: RequestHandler = async ({ url, platform }) => { - const owner = url.searchParams.get('owner'); - const repo = url.searchParams.get('repo'); - - if (!owner || !repo) { - return json({ error: 'Missing owner or repo parameter' }, { status: 400 }); - } - - const cache = createCache(platform); - const cacheKey = `${owner}/${repo}`; - const cachedData = await cache?.get('gh-contrib', cacheKey); - - if (cachedData) { - return json(JSON.parse(cachedData)); - } - - try { - const response = await fetch( - `${GithubContributorsAPIURL}?owner=${encodeURIComponent(owner)}&repo=${encodeURIComponent(repo)}` - ); - - if (!response.ok) { - return json( - { error: 'Failed to fetch GitHub contributors ' + response.statusText }, - { status: response.status } - ); - } - - const data = await response.json(); - - await cache?.put('gh-contrib', cacheKey, JSON.stringify(data)); - - return json(data); - } catch (error) { - console.error('Error fetching GitHub contributors:', error); - return json({ error: 'Failed to fetch GitHub contributors' }, { status: 500 }); - } -}; diff --git a/src/routes/api/lastfm/+server.ts b/src/routes/api/lastfm/+server.ts deleted file mode 100644 index e40d478..0000000 --- a/src/routes/api/lastfm/+server.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { env } from '$env/dynamic/private'; -import { createCache } from '$lib/cache'; - -const LASTFM_API_URL = 'https://ws.audioscrobbler.com/2.0/'; - -const ALLOWED_METHODS = [ - 'user.getRecentTracks', - 'user.getTopTracks', - 'user.getTopAlbums', - 'user.getInfo' -]; - -const CACHE_TTL: Record = { - 'user.getRecentTracks': 15 * 60, - 'user.getTopTracks': 60 * 60, - 'user.getTopAlbums': 60 * 60, - 'user.getInfo': 12 * 60 * 60 -}; - -export const GET: RequestHandler = async ({ url, platform }) => { - const method = url.searchParams.get('method'); - const user = url.searchParams.get('user'); - const period = url.searchParams.get('period') || '7day'; - const limit = url.searchParams.get('limit') || '50'; - - if (!method || !user) { - return json({ error: 'Missing method or user parameter' }, { status: 400 }); - } - - if (!ALLOWED_METHODS.includes(method)) { - return json({ error: 'Method not allowed' }, { status: 400 }); - } - - const cache = createCache(platform); - const cacheKey = `${method}:${user}:${period}:${limit}`; - const cachedData = await cache?.get('lastfm', cacheKey); - - if (cachedData) { - return json(JSON.parse(cachedData)); - } - - const apiKey = env?.LASTFM_API_KEY; - if (!apiKey) { - return json({ error: 'Last.fm API key not configured' }, { status: 500 }); - } - - try { - const params = new URLSearchParams({ - method, - user, - api_key: apiKey, - format: 'json', - limit - }); - - if (method === 'user.getTopTracks' || method === 'user.getTopAlbums') { - params.set('period', period); - } - - const response = await fetch(`${LASTFM_API_URL}?${params}`); - - if (!response.ok) { - return json( - { error: 'Failed to fetch Last.fm data: ' + response.statusText }, - { status: response.status } - ); - } - - const data = await response.json(); - - if (data.error) { - return json({ error: data.message || 'Last.fm API error' }, { status: 400 }); - } - - const ttl = CACHE_TTL[method] || 60 * 60; - await cache?.put('lastfm', cacheKey, JSON.stringify(data), ttl); - - return json(data); - } catch (error) { - console.error('Error fetching Last.fm data:', error); - return json({ error: 'Failed to fetch Last.fm data' }, { status: 500 }); - } -}; diff --git a/src/routes/api/npmx-leaderboard/+server.ts b/src/routes/api/npmx-leaderboard/+server.ts deleted file mode 100644 index 17711e7..0000000 --- a/src/routes/api/npmx-leaderboard/+server.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { json } from '@sveltejs/kit'; -import type { RequestHandler } from './$types'; -import { createCache } from '$lib/cache'; - -const LEADERBOARD_API_URL = - 'https://npmx-likes-leaderboard-api-production.up.railway.app/api/leaderboard/likes?limit=20'; - -export const GET: RequestHandler = async ({ platform }) => { - const cache = createCache(platform); - const cachedData = await cache?.get('npmx', 'likes'); - - if (cachedData) { - return json(JSON.parse(cachedData)); - } - - try { - const response = await fetch(LEADERBOARD_API_URL); - - if (!response.ok) { - return json( - { error: 'Failed to fetch npmx leaderboard ' + response.statusText }, - { status: response.status } - ); - } - - const data = await response.json(); - - await cache?.put('npmx', 'likes', JSON.stringify(data)); - - return json(data); - } catch (error) { - console.error('Error fetching npmx leaderboard:', error); - return json({ error: 'Failed to fetch npmx leaderboard' }, { status: 500 }); - } -}; -- 2.51.2 From eea985cbb7ffc67c52e06a39453f4c0126d27d9f Mon Sep 17 00:00:00 2001 From: Florian <45694132+flo-bit@users.noreply.github.com> Date: Sun, 8 Feb 2026 17:46:03 +0100 Subject: [PATCH 7/7] update claude.md --- CLAUDE.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 39b2963..94ea249 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -53,7 +53,9 @@ Grid margins: 16px desktop, 12px mobile. **Card System (`src/lib/cards/`):** - `CardDefinition` type in `types.ts` defines the interface for card types -- Each card type exports a definition with: `type`, `contentComponent`, optional `editingContentComponent`, `creationModalComponent`, `sidebarButtonText`, `loadData`, `upload` (see more info and description in `src/lib/cards/types.ts`) +- Each card type exports a definition with: `type`, `contentComponent`, optional `editingContentComponent`, `creationModalComponent`, `sidebarButtonText`, `loadData`, `loadDataServer`, `upload` (see more info and description in `src/lib/cards/types.ts`) +- `loadData` fetches external data on the client (via remote functions). `loadDataServer` is the server-side equivalent used during SSR to avoid self-referential HTTP requests on Cloudflare Workers. +- Cards that need external data use `.remote.ts` files (SvelteKit remote functions) co-located in the card folder (e.g. `GitHubProfileCard/api.remote.ts`, `LastFMCard/api.remote.ts`). These use `query()` from `$app/server` and run server-side, with SvelteKit generating fetch wrappers for client use. - Card types include Text, Link, Image, Bluesky, Embed, Map, Livestream, ATProto collections, and special cards (see `src/lib/cards`). - `AllCardDefinitions` and `CardDefinitionsByType` in `index.ts` aggregate all card types - See e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation. @@ -69,11 +71,12 @@ Grid margins: 16px desktop, 12px mobile. **Caching (`src/lib/cache.ts`):** - `CacheService` class wraps a single Cloudflare KV namespace (`USER_DATA_CACHE`) with namespaced keys -- Keys are stored as `namespace:key` (e.g. `user:did:plc:abc`, `github:someuser`, `lastfm:method:user:period:limit`) -- Per-namespace default TTLs via KV `expirationTtl`: `user` (24h), `identity` (7d), `github` (12h), `gh-contrib` (12h), `lastfm` (1h, overridable), `npmx` (12h), `meta` (no expiry) -- User data is keyed by DID with bidirectional handle↔DID identity mappings (`identity:h:{handle}` → DID, `identity:d:{did}` → handle) -- `getUser(identifier)` accepts either a handle or DID and resolves automatically -- `putUser(did, handle, data)` writes data + both identity mappings +- Keys are stored as `namespace:key` (e.g. `blento:did:plc:abc`, `github:someuser`, `lastfm:method:user:period:limit`) +- Per-namespace default TTLs via KV `expirationTtl`: `blento` (24h), `identity` (7d), `github` (12h), `gh-contrib` (12h), `lastfm` (1h, overridable), `npmx` (12h), `meta` (no expiry) +- Blento data is keyed by DID with bidirectional handle↔DID identity mappings (`identity:h:{handle}` → DID, `identity:d:{did}` → handle) +- `getBlento(identifier)` accepts either a handle or DID and resolves automatically +- `putBlento(did, handle, data)` writes data + both identity mappings +- Generic `get(namespace, key)` / `put(namespace, key, value, ttl?)` and JSON variants `getJSON` / `putJSON` for all namespaces - `createCache(platform)` factory function creates a `CacheService` from `platform.env` - `CUSTOM_DOMAINS` KV namespace is separate and accessed directly for custom domain → DID resolution @@ -91,7 +94,7 @@ Grid margins: 16px desktop, 12px mobile. - `/edit` - Self-hosted edit mode - `/api/links` - Link preview API - `/api/geocoding` - Geocoding API for map cards -- `/api/instagram`, `/api/reloadRecent`, `/api/update` - Additional data endpoints +- `/api/reloadRecent`, `/api/update` - Additional data endpoints ### Item Type