···53**Card System (`src/lib/cards/`):**
5455- `CardDefinition` type in `types.ts` defines the interface for card types
56-- 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`)
0057- Card types include Text, Link, Image, Bluesky, Embed, Map, Livestream, ATProto collections, and special cards (see `src/lib/cards`).
58- `AllCardDefinitions` and `CardDefinitionsByType` in `index.ts` aggregate all card types
59- See e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation.
···66- Data is stored in user's PDS under collection `app.blento.card`
67- **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers.
6800000000000069**Data Loading (`src/lib/website/`):**
7071-- `load.ts` - Fetches user data from their PDS, with optional KV caching via `UserCache`
72- `data.ts` - Defines which collections/records to fetch
73- `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree
74···80- `/edit` - Self-hosted edit mode
81- `/api/links` - Link preview API
82- `/api/geocoding` - Geocoding API for map cards
83-- `/api/instagram`, `/api/reloadRecent`, `/api/update` - Additional data endpoints
8485### Item Type
86
···53**Card System (`src/lib/cards/`):**
5455- `CardDefinition` type in `types.ts` defines the interface for card types
56+- 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`)
57+- `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.
58+- 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.
59- Card types include Text, Link, Image, Bluesky, Embed, Map, Livestream, ATProto collections, and special cards (see `src/lib/cards`).
60- `AllCardDefinitions` and `CardDefinitionsByType` in `index.ts` aggregate all card types
61- See e.g. `src/lib/cards/EmbedCard/` and `src/lib/cards/LivestreamCard/` for examples of implementation.
···68- Data is stored in user's PDS under collection `app.blento.card`
69- **Important**: ATProto does not allow floating point numbers in records. All numeric values must be integers.
7071+**Caching (`src/lib/cache.ts`):**
72+73+- `CacheService` class wraps a single Cloudflare KV namespace (`USER_DATA_CACHE`) with namespaced keys
74+- Keys are stored as `namespace:key` (e.g. `blento:did:plc:abc`, `github:someuser`, `lastfm:method:user:period:limit`)
75+- Per-namespace default TTLs via KV `expirationTtl`: `blento` (24h), `identity` (7d), `github` (12h), `gh-contrib` (12h), `lastfm` (1h, overridable), `npmx` (12h), `meta` (no expiry)
76+- Blento data is keyed by DID with bidirectional handleโDID identity mappings (`identity:h:{handle}` โ DID, `identity:d:{did}` โ handle)
77+- `getBlento(identifier)` accepts either a handle or DID and resolves automatically
78+- `putBlento(did, handle, data)` writes data + both identity mappings
79+- Generic `get(namespace, key)` / `put(namespace, key, value, ttl?)` and JSON variants `getJSON` / `putJSON` for all namespaces
80+- `createCache(platform)` factory function creates a `CacheService` from `platform.env`
81+- `CUSTOM_DOMAINS` KV namespace is separate and accessed directly for custom domain โ DID resolution
82+83**Data Loading (`src/lib/website/`):**
8485+- `load.ts` - Fetches user data from their PDS, with optional caching via `CacheService`
86- `data.ts` - Defines which collections/records to fetch
87- `context.ts` - Svelte contexts for passing DID, handle, and data down the component tree
88···94- `/edit` - Self-hosted edit mode
95- `/api/links` - Link preview API
96- `/api/geocoding` - Geocoding API for map cards
97+- `/api/reloadRecent`, `/api/update` - Additional data endpoints
9899### Item Type
100
+1-1
docs/Selfhosting.md
···356. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker
3637- github profile: GITHUB_TOKEN (token with public_repo access)
38-- map: PUBLIC_MAPBOX_TOKEN
···356. some cards need their own additional env keys, if you have these cards in your profile, create your keys and add them to your cloudflare worker
3637- github profile: GITHUB_TOKEN (token with public_repo access)
38+- map: no token needed (uses OpenFreeMap)
···1import type { Component } from 'svelte';
2-import type { Item, UserCache } from '$lib/types';
03import type { Did } from '@atcute/lexicons';
45export type CreationModalComponentProps = {
···36 loadData?: (
37 // all cards of that type
38 items: Item[],
39- { did, handle, cache }: { did: Did; handle: string; cache?: UserCache }
0000000000000000040 ) => Promise<unknown>;
4142 // show color selection popup
···1import type { Component } from 'svelte';
2+import type { Item } from '$lib/types';
3+import type { CacheService } from '$lib/cache';
4import type { Did } from '@atcute/lexicons';
56export type CreationModalComponentProps = {
···37 loadData?: (
38 // all cards of that type
39 items: Item[],
40+ { did, handle, cache }: { did: Did; handle: string; cache?: CacheService }
41+ ) => Promise<unknown>;
42+43+ // server-side version of loadData that calls external APIs directly
44+ // instead of going through self-referential /api/ routes (avoids 522 on Cloudflare Workers)
45+ loadDataServer?: (
46+ items: Item[],
47+ {
48+ did,
49+ handle,
50+ cache,
51+ env
52+ }: {
53+ did: Did;
54+ handle: string;
55+ cache?: CacheService;
56+ env?: Record<string, string | undefined>;
57+ }
58 ) => Promise<unknown>;
5960 // show color selection popup