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