+20
-11
src/leaflet-live-loader.ts
+20
-11
src/leaflet-live-loader.ts
···
12
12
CollectionFilter,
13
13
EntryFilter,
14
14
LeafletRecord,
15
-
LiveLoaderOptions,
15
+
LeafletLoaderOptions,
16
16
} from "./types.js";
17
17
18
18
export class LiveLoaderError extends Error {
···
34
34
*/
35
35
36
36
export function leafletLiveLoader(
37
-
options: LiveLoaderOptions,
37
+
options: LeafletLoaderOptions,
38
38
): LiveLoader<LeafletRecord, EntryFilter, CollectionFilter, LiveLoaderError> {
39
39
const { repo } = options;
40
40
···
56
56
}
57
57
58
58
return {
59
-
name: "leaflet-live-loader",
59
+
name: "leaflet-loader-astro",
60
60
loadCollection: async ({ filter }) => {
61
61
try {
62
62
const pds_url = await resolveMiniDoc(repo);
63
63
const agent = new Agent({ service: pds_url });
64
64
65
65
const documents = await getLeafletDocuments({
66
-
repo,
67
66
agent,
67
+
repo,
68
+
reverse: filter?.reverse,
68
69
cursor: filter?.cursor,
69
70
limit: filter?.limit,
70
-
reverse: filter?.reverse,
71
71
});
72
72
73
73
return {
74
-
entries: documents.map((document) => ({
75
-
id: uriToRkey(document.uri),
76
-
data: document,
77
-
})),
74
+
entries: documents.map((document) => {
75
+
const id = uriToRkey(document.uri);
76
+
return {
77
+
id,
78
+
data: {
79
+
id,
80
+
...document,
81
+
},
82
+
};
83
+
}),
78
84
};
79
85
} catch (error) {
80
86
return {
···
104
110
});
105
111
106
112
return {
107
-
id: uriToRkey(document.data.uri),
108
-
data: document.data.value,
113
+
id: filter?.id,
114
+
data: {
115
+
id: filter?.id,
116
+
...document,
117
+
},
109
118
};
110
119
} catch {
111
120
return {
+181
src/test.ts
+181
src/test.ts
···
1
+
import {
2
+
AtpAgent,
3
+
type AppBskyFeedGetAuthorFeed,
4
+
type AppBskyFeedDefs,
5
+
} from "@atproto/api";
6
+
import type { LiveLoader } from "astro/loaders";
7
+
8
+
export interface LiveBlueskyLoaderOptions {
9
+
identifier?: string;
10
+
service?: string;
11
+
}
12
+
13
+
export interface CollectionFilter {
14
+
limit?: number;
15
+
since?: Date;
16
+
until?: Date;
17
+
type?: AppBskyFeedGetAuthorFeed.QueryParams["filter"];
18
+
identifier?: string;
19
+
}
20
+
21
+
export interface EntryFilter {
22
+
id?: string;
23
+
}
24
+
25
+
export class BlueskyError extends Error {
26
+
constructor(
27
+
message: string,
28
+
public code?: string,
29
+
public identifier?: string,
30
+
) {
31
+
super(message);
32
+
this.name = "BlueskyError";
33
+
}
34
+
}
35
+
36
+
export function liveBlueskyLoader(
37
+
options: LiveBlueskyLoaderOptions = {},
38
+
): LiveLoader<
39
+
AppBskyFeedDefs.PostView,
40
+
EntryFilter,
41
+
CollectionFilter,
42
+
BlueskyError
43
+
> {
44
+
const {
45
+
identifier: defaultIdentifier,
46
+
service = "https://public.api.bsky.app",
47
+
} = options;
48
+
49
+
return {
50
+
name: "live-bluesky-loader",
51
+
52
+
loadCollection: async ({ filter }) => {
53
+
try {
54
+
const identifier = filter?.identifier || defaultIdentifier;
55
+
56
+
if (!identifier) {
57
+
return {
58
+
error: new BlueskyError(
59
+
"Identifier must be provided either in loader options or collection filter",
60
+
"MISSING_IDENTIFIER",
61
+
),
62
+
};
63
+
}
64
+
65
+
const agent = new AtpAgent({ service });
66
+
67
+
let cursor = undefined;
68
+
const allPosts: AppBskyFeedDefs.PostView[] = [];
69
+
let count = 0;
70
+
71
+
do {
72
+
const { data } = await agent.getAuthorFeed({
73
+
actor: identifier,
74
+
filter: filter?.type,
75
+
cursor,
76
+
limit: 100,
77
+
});
78
+
79
+
for (const { post } of data.feed) {
80
+
// Apply collection filters
81
+
if (filter?.limit && count >= filter.limit) {
82
+
break;
83
+
}
84
+
85
+
if (filter?.since) {
86
+
const postDate = new Date(post.indexedAt);
87
+
if (postDate < filter.since) {
88
+
continue;
89
+
}
90
+
}
91
+
92
+
if (filter?.until) {
93
+
const postDate = new Date(post.indexedAt);
94
+
if (postDate > filter.until) {
95
+
continue;
96
+
}
97
+
}
98
+
99
+
allPosts.push(post);
100
+
count++;
101
+
}
102
+
103
+
cursor = data.cursor;
104
+
} while (cursor && (!filter?.limit || count < filter.limit));
105
+
106
+
return {
107
+
entries: allPosts.map((post) => ({
108
+
id: post.uri,
109
+
data: post,
110
+
// rendered: {
111
+
// html: renderPostAsHtml(post),
112
+
// },
113
+
})),
114
+
};
115
+
} catch (error) {
116
+
const identifier = filter?.identifier || defaultIdentifier;
117
+
return {
118
+
error: new BlueskyError(
119
+
`Failed to load Bluesky posts for ${identifier || "unknown"}`,
120
+
"COLLECTION_LOAD_ERROR",
121
+
identifier,
122
+
),
123
+
};
124
+
}
125
+
},
126
+
127
+
loadEntry: async ({ filter }) => {
128
+
try {
129
+
const agent = new AtpAgent({ service });
130
+
131
+
if (!filter.id) {
132
+
return {
133
+
error: new BlueskyError(
134
+
"'id' must be provided in the filter",
135
+
"INVALID_FILTER",
136
+
),
137
+
};
138
+
}
139
+
140
+
// Validate that the ID is a full AT URI
141
+
if (!filter.id.startsWith("at://")) {
142
+
return {
143
+
error: new BlueskyError(
144
+
`Invalid ID format: '${filter.id}'. Must be a full AT URI (e.g., 'at://did:plc:user/app.bsky.feed.post/id')`,
145
+
"INVALID_ID_FORMAT",
146
+
),
147
+
};
148
+
}
149
+
150
+
const postUri = filter.id;
151
+
152
+
// Fetch the post directly using getPosts
153
+
const { data } = await agent.getPosts({ uris: [postUri] });
154
+
155
+
const [post] = data.posts;
156
+
157
+
if (!post) {
158
+
return;
159
+
}
160
+
161
+
return {
162
+
id: post.uri,
163
+
data: post,
164
+
// rendered: {
165
+
// html: renderPostAsHtml(post),
166
+
// },
167
+
};
168
+
} catch (error) {
169
+
const errorMessage =
170
+
error instanceof Error ? error.message : "Unknown error";
171
+
const requestedUri = filter.id || "unknown";
172
+
return {
173
+
error: new BlueskyError(
174
+
`Failed to load Bluesky post '${requestedUri}': ${errorMessage}`,
175
+
"ENTRY_LOAD_ERROR",
176
+
),
177
+
};
178
+
}
179
+
},
180
+
};
181
+
}
+2
-2
src/types.ts
+2
-2
src/types.ts
···
1
1
import type { Agent } from "@atproto/api";
2
2
3
-
export interface LiveLoaderOptions {
3
+
export interface LeafletLoaderOptions {
4
4
/**
5
5
* @description Your repo is either your handle (@you.some.url) or your DID (did:plc... or did:web...). You can find this information using: https://pdsls.dev
6
6
*/
···
10
10
export interface LeafletRecord {
11
11
id: string;
12
12
uri: string;
13
-
cid: string;
13
+
cid?: string;
14
14
value: unknown;
15
15
}
16
16
+2
-2
src/utils.ts
+2
-2
src/utils.ts
···
6
6
} from "./types.js";
7
7
import { LiveLoaderError } from "./leaflet-live-loader.js";
8
8
9
-
export function uriToRkey(uri: string) {
9
+
export function uriToRkey(uri: string): string {
10
10
const rkey = uri.split("/").pop();
11
11
if (!rkey) {
12
12
throw new Error("Failed to get rkey from uri.");
···
76
76
);
77
77
}
78
78
79
-
return response;
79
+
return response?.data;
80
80
}