@ewanc26/atproto 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,188 @@
1
+ import { cache } from './cache.js';
2
+
3
+ interface MusicBrainzRelease {
4
+ id: string;
5
+ score: number;
6
+ title: string;
7
+ }
8
+
9
+ interface MusicBrainzSearchResponse {
10
+ releases: MusicBrainzRelease[];
11
+ }
12
+
13
+ interface iTunesSearchResponse {
14
+ resultCount: number;
15
+ results: Array<{ artworkUrl100?: string }>;
16
+ }
17
+
18
+ export async function searchMusicBrainzRelease(
19
+ trackName: string,
20
+ artistName: string,
21
+ releaseName?: string
22
+ ): Promise<string | null> {
23
+ const cacheKey = `mb:release:${trackName}:${artistName}:${releaseName || 'none'}`;
24
+ const cached = cache.get<string | null>(cacheKey);
25
+ if (cached !== null) return cached;
26
+
27
+ try {
28
+ if (releaseName) {
29
+ const result = await searchByReleaseName(releaseName, artistName);
30
+ if (result) { cache.set(cacheKey, result); return result; }
31
+ }
32
+ const result = await searchByTrackName(trackName, artistName);
33
+ if (result) { cache.set(cacheKey, result); return result; }
34
+ cache.set(cacheKey, null);
35
+ return null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ async function searchByReleaseName(releaseName: string, artistName: string): Promise<string | null> {
42
+ try {
43
+ const query = `release:"${releaseName}" AND artist:"${artistName}"`;
44
+ const url = `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(query)}&fmt=json&limit=5`;
45
+ const response = await fetch(url, {
46
+ headers: { 'User-Agent': 'ewancroft.uk/1.0.0 (https://ewancroft.uk)', Accept: 'application/json' }
47
+ });
48
+ if (!response.ok) return null;
49
+ const data: MusicBrainzSearchResponse = await response.json();
50
+ if (!data.releases?.length) return null;
51
+ const best = data.releases[0];
52
+ if (best.score < 80) return null;
53
+ return best.id;
54
+ } catch { return null; }
55
+ }
56
+
57
+ async function searchByTrackName(trackName: string, artistName: string): Promise<string | null> {
58
+ try {
59
+ const query = `recording:"${trackName}" AND artist:"${artistName}"`;
60
+ const url = `https://musicbrainz.org/ws/2/release/?query=${encodeURIComponent(query)}&fmt=json&limit=5`;
61
+ const response = await fetch(url, {
62
+ headers: { 'User-Agent': 'ewancroft.uk/1.0.0 (https://ewancroft.uk)', Accept: 'application/json' }
63
+ });
64
+ if (!response.ok) return null;
65
+ const data: MusicBrainzSearchResponse = await response.json();
66
+ if (!data.releases?.length) return null;
67
+ const best = data.releases[0];
68
+ if (best.score < 75) return null;
69
+ return best.id;
70
+ } catch { return null; }
71
+ }
72
+
73
+ export function buildCoverArtUrl(releaseMbId: string, size: 250 | 500 | 1200 = 500): string {
74
+ return `https://coverartarchive.org/release/${releaseMbId}/front-${size}`;
75
+ }
76
+
77
+ export async function searchiTunesArtwork(
78
+ trackName: string,
79
+ artistName: string,
80
+ releaseName?: string
81
+ ): Promise<string | null> {
82
+ const cacheKey = `itunes:artwork:${trackName}:${artistName}:${releaseName || 'none'}`;
83
+ const cached = cache.get<string | null>(cacheKey);
84
+ if (cached !== null) return cached;
85
+
86
+ try {
87
+ const searchTerm = releaseName ? `${releaseName} ${artistName}` : `${trackName} ${artistName}`;
88
+ const url = `https://itunes.apple.com/search?term=${encodeURIComponent(searchTerm)}&entity=album&limit=5`;
89
+ const response = await fetch(url);
90
+ if (!response.ok) { cache.set(cacheKey, null); return null; }
91
+ const data: iTunesSearchResponse = await response.json();
92
+ if (!data.results?.length) { cache.set(cacheKey, null); return null; }
93
+ let artworkUrl = data.results[0].artworkUrl100;
94
+ if (artworkUrl) {
95
+ artworkUrl = artworkUrl.replace('100x100', '600x600');
96
+ cache.set(cacheKey, artworkUrl);
97
+ return artworkUrl;
98
+ }
99
+ cache.set(cacheKey, null);
100
+ return null;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ export async function searchDeezerArtwork(
107
+ trackName: string,
108
+ artistName: string,
109
+ releaseName?: string
110
+ ): Promise<string | null> {
111
+ // Deezer has CORS restrictions in browsers — skip silently
112
+ return null;
113
+ }
114
+
115
+ export async function searchLastFmArtwork(
116
+ trackName: string,
117
+ artistName: string,
118
+ releaseName?: string
119
+ ): Promise<string | null> {
120
+ if (!releaseName) return null;
121
+
122
+ const cacheKey = `lastfm:artwork:${trackName}:${artistName}:${releaseName}`;
123
+ const cached = cache.get<string | null>(cacheKey);
124
+ if (cached !== null) return cached;
125
+
126
+ try {
127
+ const url = `https://ws.audioscrobbler.com/2.0/?method=album.getinfo&api_key=8de8b91ab0c3f8d08a35c33bf0e0e803&artist=${encodeURIComponent(artistName)}&album=${encodeURIComponent(releaseName)}&format=json`;
128
+ const response = await fetch(url);
129
+ if (!response.ok) { cache.set(cacheKey, null); return null; }
130
+ const data: any = await response.json();
131
+ if (!data.album?.image) { cache.set(cacheKey, null); return null; }
132
+ const images = data.album.image;
133
+ const largeImage =
134
+ images.find((img: any) => img.size === 'extralarge') ||
135
+ images.find((img: any) => img.size === 'large') ||
136
+ images.find((img: any) => img.size === 'medium');
137
+ if (largeImage?.['#text']) {
138
+ cache.set(cacheKey, largeImage['#text']);
139
+ return largeImage['#text'];
140
+ }
141
+ cache.set(cacheKey, null);
142
+ return null;
143
+ } catch {
144
+ return null;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Cascading artwork search: Cover Art Archive → MusicBrainz+CAA → iTunes → Last.fm
150
+ */
151
+ export async function findArtwork(
152
+ trackName: string,
153
+ artistName: string,
154
+ releaseName?: string,
155
+ releaseMbId?: string,
156
+ fetchFn?: typeof fetch
157
+ ): Promise<string | null> {
158
+ const _fetch = fetchFn || globalThis.fetch;
159
+
160
+ // 1. Try Cover Art Archive with known MBID
161
+ if (releaseMbId) {
162
+ const caaUrl = buildCoverArtUrl(releaseMbId, 500);
163
+ try {
164
+ const res = await _fetch(caaUrl, { method: 'HEAD' });
165
+ if (res.ok) return caaUrl;
166
+ } catch { /* continue */ }
167
+ }
168
+
169
+ // 2. Search MusicBrainz for MBID, then try CAA
170
+ const mbId = await searchMusicBrainzRelease(trackName, artistName, releaseName);
171
+ if (mbId) {
172
+ const caaUrl = buildCoverArtUrl(mbId, 500);
173
+ try {
174
+ const res = await _fetch(caaUrl, { method: 'HEAD' });
175
+ if (res.ok) return caaUrl;
176
+ } catch { /* continue */ }
177
+ }
178
+
179
+ // 3. Try iTunes
180
+ const iTunesUrl = await searchiTunesArtwork(trackName, artistName, releaseName);
181
+ if (iTunesUrl) return iTunesUrl;
182
+
183
+ // 4. Try Last.fm
184
+ const lastFmUrl = await searchLastFmArtwork(trackName, artistName, releaseName);
185
+ if (lastFmUrl) return lastFmUrl;
186
+
187
+ return null;
188
+ }
@@ -0,0 +1,57 @@
1
+ import { withFallback } from '../agents.js';
2
+
3
+ export interface FetchRecordsConfig {
4
+ repo: string;
5
+ collection: string;
6
+ limit?: number;
7
+ fetchFn?: typeof fetch;
8
+ }
9
+
10
+ export interface AtProtoRecord<T = any> {
11
+ uri: string;
12
+ value: T;
13
+ cid?: string;
14
+ }
15
+
16
+ export async function fetchAllRecords<T = any>(
17
+ config: FetchRecordsConfig
18
+ ): Promise<AtProtoRecord<T>[]> {
19
+ const { repo, collection, limit = 100, fetchFn } = config;
20
+ const allRecords: AtProtoRecord<T>[] = [];
21
+ let cursor: string | undefined;
22
+
23
+ try {
24
+ do {
25
+ const records = await withFallback(
26
+ repo,
27
+ async (agent) => {
28
+ const response = await agent.com.atproto.repo.listRecords({
29
+ repo,
30
+ collection,
31
+ limit,
32
+ cursor
33
+ });
34
+ cursor = response.data.cursor;
35
+ return response.data.records;
36
+ },
37
+ true,
38
+ fetchFn
39
+ );
40
+ allRecords.push(...(records as AtProtoRecord<T>[]));
41
+ } while (cursor);
42
+ } catch (error) {
43
+ console.warn(`Failed to fetch records from ${collection}:`, error);
44
+ throw error;
45
+ }
46
+
47
+ return allRecords;
48
+ }
49
+
50
+ export async function fetchAllUserRecords<T = any>(
51
+ did: string,
52
+ collection: string,
53
+ fetchFn?: typeof fetch,
54
+ limit?: number
55
+ ): Promise<AtProtoRecord<T>[]> {
56
+ return fetchAllRecords<T>({ repo: did, collection, limit, fetchFn });
57
+ }
@@ -0,0 +1,2 @@
1
+ export { fetchAllRecords, fetchAllUserRecords } from './fetchAllRecords.js';
2
+ export type { FetchRecordsConfig, AtProtoRecord } from './fetchAllRecords.js';
package/src/posts.ts ADDED
@@ -0,0 +1,197 @@
1
+ import { cache } from './cache.js';
2
+ import { withFallback } from './agents.js';
3
+ import type { BlueskyPost, PostAuthor, ExternalLink } from './types.js';
4
+
5
+ export async function fetchLatestBlueskyPost(
6
+ did: string,
7
+ fetchFn?: typeof fetch
8
+ ): Promise<BlueskyPost | null> {
9
+ const cacheKey = `blueskypost:latest:${did}`;
10
+ const cached = cache.get<BlueskyPost>(cacheKey);
11
+ if (cached) return cached;
12
+
13
+ try {
14
+ const feedResponse = await withFallback(
15
+ did,
16
+ async (agent) => agent.getAuthorFeed({ actor: did, limit: 5 }),
17
+ false,
18
+ fetchFn
19
+ );
20
+
21
+ const feed = feedResponse.data.feed;
22
+ if (!feed.length) return null;
23
+
24
+ const latestFeedItem = feed[0];
25
+ const latestPostData = latestFeedItem.post;
26
+
27
+ const isRepost = latestFeedItem.reason?.$type === 'app.bsky.feed.defs#reasonRepost';
28
+ let repostAuthor: PostAuthor | undefined;
29
+ let repostCreatedAt: string | undefined;
30
+
31
+ if (isRepost && latestFeedItem.reason) {
32
+ const reason = latestFeedItem.reason as any;
33
+ repostAuthor = {
34
+ did: reason.by.did,
35
+ handle: reason.by.handle,
36
+ displayName: reason.by.displayName,
37
+ avatar: reason.by.avatar
38
+ };
39
+ repostCreatedAt = reason.indexedAt;
40
+ }
41
+
42
+ const post = await fetchPostFromUri(did, latestPostData.uri, 0, fetchFn);
43
+ if (!post) return null;
44
+
45
+ if (isRepost) {
46
+ post.isRepost = true;
47
+ post.repostAuthor = repostAuthor;
48
+ post.repostCreatedAt = repostCreatedAt;
49
+ post.originalPost = { ...post };
50
+ }
51
+
52
+ cache.set(cacheKey, post);
53
+ return post;
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+
59
+ export async function fetchPostFromUri(
60
+ did: string,
61
+ uri: string,
62
+ depth: number,
63
+ fetchFn?: typeof fetch
64
+ ): Promise<BlueskyPost | null> {
65
+ if (depth >= 3) return null;
66
+
67
+ try {
68
+ const threadResponse = await withFallback(
69
+ did,
70
+ async (agent) => agent.getPostThread({ uri, depth: 0 }),
71
+ false,
72
+ fetchFn
73
+ );
74
+
75
+ if (!threadResponse.data.thread || !('post' in threadResponse.data.thread)) return null;
76
+
77
+ const postData = threadResponse.data.thread.post;
78
+ const value = postData.record as any;
79
+ const embed = (postData as any).embed ?? null;
80
+
81
+ const author: PostAuthor = {
82
+ did: postData.author.did,
83
+ handle: postData.author.handle,
84
+ displayName: postData.author.displayName,
85
+ avatar: postData.author.avatar
86
+ };
87
+
88
+ let imageUrls: string[] | undefined;
89
+ let imageAlts: string[] | undefined;
90
+ let hasImages = false;
91
+ let hasVideo = false;
92
+ let videoUrl: string | undefined;
93
+ let videoThumbnail: string | undefined;
94
+ let quotedPost: BlueskyPost | undefined;
95
+ let quotedPostUri: string | undefined;
96
+ let externalLink: ExternalLink | undefined;
97
+
98
+ const facets = value.facets;
99
+
100
+ if (embed?.$type === 'app.bsky.embed.images#view' && Array.isArray(embed.images)) {
101
+ hasImages = true;
102
+ imageUrls = [];
103
+ imageAlts = [];
104
+ for (const img of embed.images) {
105
+ const imageUrl = img.fullsize || img.thumb;
106
+ if (imageUrl) { imageUrls.push(imageUrl); imageAlts.push(img.alt || ''); }
107
+ }
108
+ }
109
+
110
+ if (embed?.$type === 'app.bsky.embed.video#view') {
111
+ if (embed.playlist) { hasVideo = true; videoUrl = embed.playlist; }
112
+ if (embed.thumbnail) videoThumbnail = embed.thumbnail;
113
+ }
114
+
115
+ if (embed?.$type === 'app.bsky.embed.external#view' && embed.external) {
116
+ externalLink = {
117
+ uri: embed.external.uri,
118
+ title: embed.external.title,
119
+ description: embed.external.description,
120
+ thumb: embed.external.thumb
121
+ };
122
+ }
123
+
124
+ if (embed?.$type === 'app.bsky.embed.recordWithMedia#view') {
125
+ const media = embed.media;
126
+ if (media?.$type === 'app.bsky.embed.images#view' && Array.isArray(media.images)) {
127
+ hasImages = true;
128
+ imageUrls = [];
129
+ imageAlts = [];
130
+ for (const img of media.images) {
131
+ const imageUrl = img.fullsize || img.thumb;
132
+ if (imageUrl) { imageUrls.push(imageUrl); imageAlts.push(img.alt || ''); }
133
+ }
134
+ }
135
+ if (media?.$type === 'app.bsky.embed.video#view') {
136
+ if (media.playlist) { hasVideo = true; videoUrl = media.playlist; }
137
+ if (media.thumbnail) videoThumbnail = media.thumbnail;
138
+ }
139
+ if (media?.$type === 'app.bsky.embed.external#view' && media.external) {
140
+ externalLink = {
141
+ uri: media.external.uri,
142
+ title: media.external.title,
143
+ description: media.external.description,
144
+ thumb: media.external.thumb
145
+ };
146
+ }
147
+ const quotedRecord = embed.record?.record || embed.record;
148
+ if (quotedRecord?.uri) {
149
+ quotedPostUri = quotedRecord.uri as string;
150
+ quotedPost = (await fetchPostFromUri(did, quotedPostUri, depth + 1, fetchFn)) ?? undefined;
151
+ }
152
+ }
153
+
154
+ if (embed?.$type === 'app.bsky.embed.record#view') {
155
+ const quotedRecord = embed.record?.record || embed.record;
156
+ if (quotedRecord?.uri) {
157
+ quotedPostUri = quotedRecord.uri as string;
158
+ quotedPost = (await fetchPostFromUri(did, quotedPostUri, depth + 1, fetchFn)) ?? undefined;
159
+ }
160
+ }
161
+
162
+ let replyParent: BlueskyPost | undefined;
163
+ let replyRoot: BlueskyPost | undefined;
164
+ if (value.reply) {
165
+ if (value.reply.parent?.uri) {
166
+ replyParent = (await fetchPostFromUri(did, value.reply.parent.uri as string, depth + 1, fetchFn)) ?? undefined;
167
+ }
168
+ if (value.reply.root?.uri && value.reply.root.uri !== value.reply.parent?.uri) {
169
+ replyRoot = (await fetchPostFromUri(did, value.reply.root.uri as string, depth + 1, fetchFn)) ?? undefined;
170
+ }
171
+ }
172
+
173
+ return {
174
+ text: value.text,
175
+ createdAt: value.createdAt,
176
+ uri: postData.uri,
177
+ author,
178
+ likeCount: postData.likeCount || 0,
179
+ repostCount: postData.repostCount || 0,
180
+ replyCount: postData.replyCount,
181
+ hasImages,
182
+ imageUrls,
183
+ imageAlts,
184
+ hasVideo,
185
+ videoUrl,
186
+ videoThumbnail,
187
+ quotedPostUri,
188
+ quotedPost,
189
+ facets,
190
+ externalLink,
191
+ replyParent,
192
+ replyRoot
193
+ };
194
+ } catch {
195
+ return null;
196
+ }
197
+ }
package/src/types.ts ADDED
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Type definitions for AT Protocol services
3
+ * Identical to src/lib/services/atproto/types.ts — no SvelteKit dependencies.
4
+ */
5
+
6
+ export interface ProfileData {
7
+ did: string;
8
+ handle: string;
9
+ displayName?: string;
10
+ description?: string;
11
+ avatar?: string;
12
+ banner?: string;
13
+ followersCount?: number;
14
+ followsCount?: number;
15
+ postsCount?: number;
16
+ pronouns?: string;
17
+ }
18
+
19
+ export interface StatusData {
20
+ text: string;
21
+ createdAt: string;
22
+ }
23
+
24
+ export interface Technology {
25
+ name: string;
26
+ url?: string;
27
+ description?: string;
28
+ }
29
+
30
+ export interface License {
31
+ name: string;
32
+ url?: string;
33
+ }
34
+
35
+ export interface BasedOnItem {
36
+ section?: string;
37
+ name?: string;
38
+ url?: string;
39
+ description?: string;
40
+ type?: string;
41
+ }
42
+
43
+ export interface RelatedService {
44
+ section?: string;
45
+ name?: string;
46
+ url?: string;
47
+ description?: string;
48
+ relationship?: string;
49
+ }
50
+
51
+ export interface Repository {
52
+ platform?: string;
53
+ url: string;
54
+ type?: string;
55
+ description?: string;
56
+ }
57
+
58
+ export interface Credit {
59
+ section?: string;
60
+ name?: string;
61
+ type: string;
62
+ url?: string;
63
+ author?: string;
64
+ license?: License;
65
+ description?: string;
66
+ }
67
+
68
+ export interface SectionLicense {
69
+ section?: string;
70
+ name?: string;
71
+ url?: string;
72
+ }
73
+
74
+ export interface SiteInfoData {
75
+ technologyStack?: Technology[];
76
+ privacyStatement?: string;
77
+ openSourceInfo?: {
78
+ description?: string;
79
+ license?: License;
80
+ basedOn?: BasedOnItem[];
81
+ relatedServices?: RelatedService[];
82
+ repositories?: Repository[];
83
+ };
84
+ credits?: Credit[];
85
+ additionalInfo?: {
86
+ websiteBirthYear?: number;
87
+ purpose?: string;
88
+ sectionLicense?: SectionLicense[];
89
+ };
90
+ }
91
+
92
+ export interface LinkCard {
93
+ url: string;
94
+ text: string;
95
+ emoji: string;
96
+ }
97
+
98
+ export interface LinkData {
99
+ cards: LinkCard[];
100
+ }
101
+
102
+ export interface BlogPost {
103
+ title: string;
104
+ url: string;
105
+ createdAt: string;
106
+ platform: 'standard.site';
107
+ description?: string;
108
+ rkey: string;
109
+ publicationName?: string;
110
+ publicationRkey?: string;
111
+ tags?: string[];
112
+ coverImage?: string;
113
+ textContent?: string;
114
+ updatedAt?: string;
115
+ }
116
+
117
+ export interface BlogPostsData {
118
+ posts: BlogPost[];
119
+ }
120
+
121
+ export interface Facet {
122
+ index: { byteStart: number; byteEnd: number };
123
+ features: Array<{ $type: string; uri?: string; did?: string; tag?: string }>;
124
+ }
125
+
126
+ export interface ExternalLink {
127
+ uri: string;
128
+ title: string;
129
+ description?: string;
130
+ thumb?: string;
131
+ }
132
+
133
+ export interface PostAuthor {
134
+ did: string;
135
+ handle: string;
136
+ displayName?: string;
137
+ avatar?: string;
138
+ pronouns?: string;
139
+ }
140
+
141
+ export interface BlueskyPost {
142
+ text: string;
143
+ createdAt: string;
144
+ uri: string;
145
+ author: PostAuthor;
146
+ likeCount?: number;
147
+ repostCount?: number;
148
+ replyCount?: number;
149
+ hasImages: boolean;
150
+ imageUrls?: string[];
151
+ imageAlts?: string[];
152
+ hasVideo?: boolean;
153
+ videoUrl?: string;
154
+ videoThumbnail?: string;
155
+ quotedPostUri?: string;
156
+ quotedPost?: BlueskyPost;
157
+ facets?: Facet[];
158
+ externalLink?: ExternalLink;
159
+ replyParent?: BlueskyPost;
160
+ replyRoot?: BlueskyPost;
161
+ isRepost?: boolean;
162
+ repostAuthor?: PostAuthor;
163
+ repostCreatedAt?: string;
164
+ originalPost?: BlueskyPost;
165
+ }
166
+
167
+ export interface ResolvedIdentity {
168
+ did: string;
169
+ pds: string;
170
+ }
171
+
172
+ export interface CacheEntry<T> {
173
+ data: T;
174
+ timestamp: number;
175
+ }
176
+
177
+ export interface MusicArtist {
178
+ artistName: string;
179
+ artistMbId?: string;
180
+ }
181
+
182
+ export interface MusicStatusData {
183
+ trackName: string;
184
+ artists: MusicArtist[];
185
+ releaseName?: string;
186
+ playedTime: string;
187
+ originUrl?: string;
188
+ recordingMbId?: string;
189
+ releaseMbId?: string;
190
+ isrc?: string;
191
+ duration?: number;
192
+ musicServiceBaseDomain?: string;
193
+ submissionClientAgent?: string;
194
+ $type: 'fm.teal.alpha.actor.status' | 'fm.teal.alpha.feed.play';
195
+ expiry?: string;
196
+ artwork?: { ref?: { $link: string }; mimeType?: string; size?: number };
197
+ artworkUrl?: string;
198
+ }
199
+
200
+ export interface KibunStatusData {
201
+ text: string;
202
+ emoji: string;
203
+ createdAt: string;
204
+ $type: 'social.kibun.status';
205
+ }
206
+
207
+ export interface TangledRepo {
208
+ uri: string;
209
+ name: string;
210
+ description?: string;
211
+ knot: string;
212
+ createdAt: string;
213
+ labels?: string[];
214
+ source?: string;
215
+ spindle?: string;
216
+ }
217
+
218
+ export interface TangledReposData {
219
+ repos: TangledRepo[];
220
+ }
221
+
222
+ export interface StandardSiteThemeColor {
223
+ r: number;
224
+ g: number;
225
+ b: number;
226
+ a?: number;
227
+ }
228
+
229
+ export interface StandardSiteBasicTheme {
230
+ background: StandardSiteThemeColor;
231
+ foreground: StandardSiteThemeColor;
232
+ accent: StandardSiteThemeColor;
233
+ accentForeground: StandardSiteThemeColor;
234
+ }
235
+
236
+ export interface StandardSitePublication {
237
+ name: string;
238
+ rkey: string;
239
+ uri: string;
240
+ url: string;
241
+ description?: string;
242
+ icon?: string;
243
+ basicTheme?: StandardSiteBasicTheme;
244
+ preferences?: { showInDiscover?: boolean };
245
+ }
246
+
247
+ export interface StandardSitePublicationsData {
248
+ publications: StandardSitePublication[];
249
+ }
250
+
251
+ export interface StandardSiteDocument {
252
+ title: string;
253
+ rkey: string;
254
+ uri: string;
255
+ url: string;
256
+ site: string;
257
+ path?: string;
258
+ description?: string;
259
+ coverImage?: string;
260
+ content?: any;
261
+ textContent?: string;
262
+ bskyPostRef?: { uri: string; cid: string };
263
+ tags?: string[];
264
+ publishedAt: string;
265
+ updatedAt?: string;
266
+ publicationName?: string;
267
+ publicationRkey?: string;
268
+ }
269
+
270
+ export interface StandardSiteDocumentsData {
271
+ documents: StandardSiteDocument[];
272
+ }