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