A calm place to write long-form, and publish it to the open social web.
skypress.blog/
1/**
2 * Reader-side (public, no-auth) resolution of a writer's SkyPress publications (SP10, step F/G).
3 *
4 * Reads `site.standard.publication` over the public `com.atproto.repo` XRPC (`listRecords`,
5 * SSRF-guarded), keeping only publications SkyPress OWNS (`isSkyPressPublicationUrl`) so a
6 * record some other tool wrote into the shared collection never renders under a skypress.blog
7 * URL. The authed CRUD counterpart lives in `../publish/publications.ts`.
8 */
9import { listRecords } from './records';
10import {
11 isSkyPressPublicationUrl,
12 publicationSlugFromUrl,
13} from '../publish/records';
14import { detectProvider, type ProviderId } from '../publish/providers';
15import type { BlobRefJson } from '../media/blob';
16import { parseBasicTheme, type BasicTheme } from '../publish/themes';
17
18export interface ReaderPublication {
19 uri: string;
20 slug: string;
21 name: string;
22 description: string | null;
23 icon: BlobRefJson | null;
24 basicTheme: BasicTheme | null;
25}
26
27/** A publication record another app wrote into the shared collection (Leaflet, …). Read-only, links out. */
28export interface ReaderForeignPublication {
29 uri: string;
30 name: string;
31 hostname: string;
32 url: string;
33 icon: BlobRefJson | null;
34 /** The app that wrote the record (Leaflet, pckt, …), or null when unrecognised. */
35 provider: ProviderId | null;
36}
37
38interface RawPublication {
39 url?: string;
40 name?: string;
41 description?: string;
42 icon?: BlobRefJson;
43 basicTheme?: unknown;
44}
45
46function toReaderPublication( record: { uri: string; value: RawPublication } ): ReaderPublication | null {
47 const value = record.value;
48 if ( ! value?.url || ! isSkyPressPublicationUrl( value.url ) ) {
49 return null;
50 }
51 const slug = publicationSlugFromUrl( value.url );
52 if ( ! slug ) {
53 return null;
54 }
55 return {
56 uri: record.uri,
57 slug,
58 name: value.name ?? slug,
59 description: value.description?.trim() || null,
60 icon: value.icon ?? null,
61 basicTheme: parseBasicTheme( value.basicTheme ),
62 };
63}
64
65/** Map a raw record to a foreign publication, or null if its url isn't a usable http(s) link. */
66function toReaderForeignPublication(
67 record: { uri: string; value: RawPublication }
68): ReaderForeignPublication | null {
69 const value = record.value;
70 if ( ! value?.url ) {
71 return null;
72 }
73 let parsed: URL;
74 try {
75 parsed = new URL( value.url );
76 } catch {
77 return null;
78 }
79 if ( parsed.protocol !== 'http:' && parsed.protocol !== 'https:' ) {
80 return null;
81 }
82 return {
83 uri: record.uri,
84 name: value.name ?? parsed.hostname,
85 hostname: parsed.hostname,
86 url: value.url,
87 icon: value.icon ?? null,
88 provider: detectProvider( value.url, value ),
89 };
90}
91
92/**
93 * Fetch the writer's publication records once and partition them: those SkyPress OWNS
94 * (rendered under skypress.blog URLs) versus FOREIGN records another app wrote into the
95 * shared collection (Leaflet, …), which the author index links out to (Decision 0010).
96 */
97export async function listAllReaderPublications(
98 pdsUrl: string,
99 did: string
100): Promise< { owned: ReaderPublication[]; foreign: ReaderForeignPublication[] } > {
101 const records = await listRecords< RawPublication >(
102 pdsUrl,
103 did,
104 'site.standard.publication',
105 100
106 );
107 const owned: ReaderPublication[] = [];
108 const foreign: ReaderForeignPublication[] = [];
109 for ( const record of records ) {
110 if ( record.value?.url && isSkyPressPublicationUrl( record.value.url ) ) {
111 const pub = toReaderPublication( record );
112 if ( pub ) {
113 owned.push( pub );
114 }
115 continue;
116 }
117 const fp = toReaderForeignPublication( record );
118 if ( fp ) {
119 foreign.push( fp );
120 }
121 }
122 return { owned, foreign };
123}
124