···11+import {
22+ AtpAgent,
33+ type AppBskyFeedGetAuthorFeed,
44+ type AppBskyFeedDefs,
55+} from "@atproto/api";
66+import type { LiveLoader } from "astro/loaders";
77+88+export interface LiveBlueskyLoaderOptions {
99+ identifier?: string;
1010+ service?: string;
1111+}
1212+1313+export interface CollectionFilter {
1414+ limit?: number;
1515+ since?: Date;
1616+ until?: Date;
1717+ type?: AppBskyFeedGetAuthorFeed.QueryParams["filter"];
1818+ identifier?: string;
1919+}
2020+2121+export interface EntryFilter {
2222+ id?: string;
2323+}
2424+2525+export class BlueskyError extends Error {
2626+ constructor(
2727+ message: string,
2828+ public code?: string,
2929+ public identifier?: string,
3030+ ) {
3131+ super(message);
3232+ this.name = "BlueskyError";
3333+ }
3434+}
3535+3636+export function liveBlueskyLoader(
3737+ options: LiveBlueskyLoaderOptions = {},
3838+): LiveLoader<
3939+ AppBskyFeedDefs.PostView,
4040+ EntryFilter,
4141+ CollectionFilter,
4242+ BlueskyError
4343+> {
4444+ const {
4545+ identifier: defaultIdentifier,
4646+ service = "https://public.api.bsky.app",
4747+ } = options;
4848+4949+ return {
5050+ name: "live-bluesky-loader",
5151+5252+ loadCollection: async ({ filter }) => {
5353+ try {
5454+ const identifier = filter?.identifier || defaultIdentifier;
5555+5656+ if (!identifier) {
5757+ return {
5858+ error: new BlueskyError(
5959+ "Identifier must be provided either in loader options or collection filter",
6060+ "MISSING_IDENTIFIER",
6161+ ),
6262+ };
6363+ }
6464+6565+ const agent = new AtpAgent({ service });
6666+6767+ let cursor = undefined;
6868+ const allPosts: AppBskyFeedDefs.PostView[] = [];
6969+ let count = 0;
7070+7171+ do {
7272+ const { data } = await agent.getAuthorFeed({
7373+ actor: identifier,
7474+ filter: filter?.type,
7575+ cursor,
7676+ limit: 100,
7777+ });
7878+7979+ for (const { post } of data.feed) {
8080+ // Apply collection filters
8181+ if (filter?.limit && count >= filter.limit) {
8282+ break;
8383+ }
8484+8585+ if (filter?.since) {
8686+ const postDate = new Date(post.indexedAt);
8787+ if (postDate < filter.since) {
8888+ continue;
8989+ }
9090+ }
9191+9292+ if (filter?.until) {
9393+ const postDate = new Date(post.indexedAt);
9494+ if (postDate > filter.until) {
9595+ continue;
9696+ }
9797+ }
9898+9999+ allPosts.push(post);
100100+ count++;
101101+ }
102102+103103+ cursor = data.cursor;
104104+ } while (cursor && (!filter?.limit || count < filter.limit));
105105+106106+ return {
107107+ entries: allPosts.map((post) => ({
108108+ id: post.uri,
109109+ data: post,
110110+ // rendered: {
111111+ // html: renderPostAsHtml(post),
112112+ // },
113113+ })),
114114+ };
115115+ } catch (error) {
116116+ const identifier = filter?.identifier || defaultIdentifier;
117117+ return {
118118+ error: new BlueskyError(
119119+ `Failed to load Bluesky posts for ${identifier || "unknown"}`,
120120+ "COLLECTION_LOAD_ERROR",
121121+ identifier,
122122+ ),
123123+ };
124124+ }
125125+ },
126126+127127+ loadEntry: async ({ filter }) => {
128128+ try {
129129+ const agent = new AtpAgent({ service });
130130+131131+ if (!filter.id) {
132132+ return {
133133+ error: new BlueskyError(
134134+ "'id' must be provided in the filter",
135135+ "INVALID_FILTER",
136136+ ),
137137+ };
138138+ }
139139+140140+ // Validate that the ID is a full AT URI
141141+ if (!filter.id.startsWith("at://")) {
142142+ return {
143143+ error: new BlueskyError(
144144+ `Invalid ID format: '${filter.id}'. Must be a full AT URI (e.g., 'at://did:plc:user/app.bsky.feed.post/id')`,
145145+ "INVALID_ID_FORMAT",
146146+ ),
147147+ };
148148+ }
149149+150150+ const postUri = filter.id;
151151+152152+ // Fetch the post directly using getPosts
153153+ const { data } = await agent.getPosts({ uris: [postUri] });
154154+155155+ const [post] = data.posts;
156156+157157+ if (!post) {
158158+ return;
159159+ }
160160+161161+ return {
162162+ id: post.uri,
163163+ data: post,
164164+ // rendered: {
165165+ // html: renderPostAsHtml(post),
166166+ // },
167167+ };
168168+ } catch (error) {
169169+ const errorMessage =
170170+ error instanceof Error ? error.message : "Unknown error";
171171+ const requestedUri = filter.id || "unknown";
172172+ return {
173173+ error: new BlueskyError(
174174+ `Failed to load Bluesky post '${requestedUri}': ${errorMessage}`,
175175+ "ENTRY_LOAD_ERROR",
176176+ ),
177177+ };
178178+ }
179179+ },
180180+ };
181181+}
+2-2
src/types.ts
···11import type { Agent } from "@atproto/api";
2233-export interface LiveLoaderOptions {
33+export interface LeafletLoaderOptions {
44 /**
55 * @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
66 */
···1010export interface LeafletRecord {
1111 id: string;
1212 uri: string;
1313- cid: string;
1313+ cid?: string;
1414 value: unknown;
1515}
1616
+2-2
src/utils.ts
···66} from "./types.js";
77import { LiveLoaderError } from "./leaflet-live-loader.js";
8899-export function uriToRkey(uri: string) {
99+export function uriToRkey(uri: string): string {
1010 const rkey = uri.split("/").pop();
1111 if (!rkey) {
1212 throw new Error("Failed to get rkey from uri.");
···7676 );
7777 }
78787979- return response;
7979+ return response?.data;
8080}