···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
···1import type { Agent } from "@atproto/api";
23-export interface LiveLoaderOptions {
4 /**
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 */
···10export interface LeafletRecord {
11 id: string;
12 uri: string;
13- cid: string;
14 value: unknown;
15}
16
···1import type { Agent } from "@atproto/api";
23+export interface LeafletLoaderOptions {
4 /**
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 */
···10export interface LeafletRecord {
11 id: string;
12 uri: string;
13+ cid?: string;
14 value: unknown;
15}
16
+2-2
src/utils.ts
···6} from "./types.js";
7import { LiveLoaderError } from "./leaflet-live-loader.js";
89-export function uriToRkey(uri: string) {
10 const rkey = uri.split("/").pop();
11 if (!rkey) {
12 throw new Error("Failed to get rkey from uri.");
···76 );
77 }
7879- return response;
80}
···6} from "./types.js";
7import { LiveLoaderError } from "./leaflet-live-loader.js";
89+export function uriToRkey(uri: string): string {
10 const rkey = uri.split("/").pop();
11 if (!rkey) {
12 throw new Error("Failed to get rkey from uri.");
···76 );
77 }
7879+ return response?.data;
80}