@emdash-cms/plugin-atproto 0.0.1
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.
- package/package.json +36 -0
- package/src/atproto.ts +408 -0
- package/src/bluesky.ts +185 -0
- package/src/index.ts +42 -0
- package/src/sandbox-entry.ts +671 -0
- package/src/standard-site.ts +195 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* standard.site record builders
|
|
3
|
+
*
|
|
4
|
+
* Builds site.standard.publication and site.standard.document records
|
|
5
|
+
* from EmDash content.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ── Types ───────────────────────────────────────────────────────
|
|
9
|
+
|
|
10
|
+
export interface StandardPublication {
|
|
11
|
+
$type: "site.standard.publication";
|
|
12
|
+
url: string;
|
|
13
|
+
name: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface StandardDocument {
|
|
18
|
+
$type: "site.standard.document";
|
|
19
|
+
/** AT-URI of the publication record, or HTTPS URL for loose documents */
|
|
20
|
+
site: string;
|
|
21
|
+
title: string;
|
|
22
|
+
publishedAt: string;
|
|
23
|
+
/** Path component -- combined with publication URL to form canonical URL */
|
|
24
|
+
path?: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
textContent?: string;
|
|
27
|
+
tags?: string[];
|
|
28
|
+
updatedAt?: string;
|
|
29
|
+
coverImage?: BlobRefLike;
|
|
30
|
+
/** Strong reference to a Bluesky post for off-platform comments */
|
|
31
|
+
bskyPostRef?: { uri: string; cid: string };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface BlobRefLike {
|
|
35
|
+
$type: "blob";
|
|
36
|
+
ref: { $link: string };
|
|
37
|
+
mimeType: string;
|
|
38
|
+
size: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// ── Builders ────────────────────────────────────────────────────
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build a site.standard.publication record.
|
|
45
|
+
*/
|
|
46
|
+
export function buildPublication(
|
|
47
|
+
siteUrl: string,
|
|
48
|
+
siteName: string,
|
|
49
|
+
description?: string,
|
|
50
|
+
): StandardPublication {
|
|
51
|
+
return {
|
|
52
|
+
$type: "site.standard.publication",
|
|
53
|
+
url: stripTrailingSlash(siteUrl),
|
|
54
|
+
name: siteName,
|
|
55
|
+
...(description ? { description } : {}),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a site.standard.document record from EmDash content.
|
|
61
|
+
*/
|
|
62
|
+
export function buildDocument(opts: {
|
|
63
|
+
publicationUri: string;
|
|
64
|
+
content: Record<string, unknown>;
|
|
65
|
+
coverImageBlob?: BlobRefLike;
|
|
66
|
+
bskyPostRef?: { uri: string; cid: string };
|
|
67
|
+
}): StandardDocument {
|
|
68
|
+
const { publicationUri, content, coverImageBlob, bskyPostRef } = opts;
|
|
69
|
+
|
|
70
|
+
const slug = getString(content, "slug");
|
|
71
|
+
const title = getString(content, "title") || "Untitled";
|
|
72
|
+
const description = getString(content, "excerpt") || getString(content, "description");
|
|
73
|
+
const publishedAt = getString(content, "published_at") || new Date().toISOString();
|
|
74
|
+
const updatedAt = getString(content, "updated_at");
|
|
75
|
+
const tags = extractTags(content);
|
|
76
|
+
|
|
77
|
+
const doc: StandardDocument = {
|
|
78
|
+
$type: "site.standard.document",
|
|
79
|
+
site: publicationUri,
|
|
80
|
+
title,
|
|
81
|
+
publishedAt,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (slug) {
|
|
85
|
+
doc.path = `/${slug}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (description) {
|
|
89
|
+
doc.description = description;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const plainText = extractPlainText(content);
|
|
93
|
+
if (plainText) {
|
|
94
|
+
doc.textContent = plainText;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (tags.length > 0) {
|
|
98
|
+
doc.tags = tags;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (updatedAt) {
|
|
102
|
+
doc.updatedAt = updatedAt;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (coverImageBlob) {
|
|
106
|
+
doc.coverImage = coverImageBlob;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (bskyPostRef) {
|
|
110
|
+
doc.bskyPostRef = bskyPostRef;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return doc;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Helpers ─────────────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
function stripTrailingSlash(url: string): string {
|
|
119
|
+
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Pre-compiled regexes
|
|
123
|
+
const HTML_TAG_RE = /<[^>]+>/g;
|
|
124
|
+
const NBSP_RE = / /g;
|
|
125
|
+
const AMP_RE = /&/g;
|
|
126
|
+
const LT_RE = /</g;
|
|
127
|
+
const GT_RE = />/g;
|
|
128
|
+
const QUOT_RE = /"/g;
|
|
129
|
+
const APOS_RE = /'/g;
|
|
130
|
+
const WHITESPACE_RE = /\s+/g;
|
|
131
|
+
const HASH_PREFIX_RE = /^#/;
|
|
132
|
+
const MAX_TEXT_CONTENT_LENGTH = 10_000;
|
|
133
|
+
|
|
134
|
+
function getString(obj: Record<string, unknown>, key: string): string | undefined {
|
|
135
|
+
const v = obj[key];
|
|
136
|
+
return typeof v === "string" && v.length > 0 ? v : undefined;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Extract tags from content. Handles both string arrays and
|
|
141
|
+
* tag objects with a name property.
|
|
142
|
+
*/
|
|
143
|
+
function extractTags(content: Record<string, unknown>): string[] {
|
|
144
|
+
const raw = content.tags;
|
|
145
|
+
if (!Array.isArray(raw)) return [];
|
|
146
|
+
|
|
147
|
+
const tags: string[] = [];
|
|
148
|
+
for (const item of raw) {
|
|
149
|
+
if (typeof item === "string") {
|
|
150
|
+
tags.push(item.replace(HASH_PREFIX_RE, ""));
|
|
151
|
+
} else if (
|
|
152
|
+
typeof item === "object" &&
|
|
153
|
+
item !== null &&
|
|
154
|
+
"name" in item &&
|
|
155
|
+
typeof (item as Record<string, unknown>).name === "string"
|
|
156
|
+
) {
|
|
157
|
+
tags.push(((item as Record<string, unknown>).name as string).replace(HASH_PREFIX_RE, ""));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return tags;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Extract plain text from content for the textContent field.
|
|
165
|
+
* Strips HTML tags and collapses whitespace.
|
|
166
|
+
*/
|
|
167
|
+
export function extractPlainText(content: Record<string, unknown>): string | undefined {
|
|
168
|
+
// Try common content field names
|
|
169
|
+
const body =
|
|
170
|
+
getString(content, "body") || getString(content, "content") || getString(content, "text");
|
|
171
|
+
|
|
172
|
+
if (!body) return undefined;
|
|
173
|
+
|
|
174
|
+
// Strip HTML tags (simple -- not a full parser, but sufficient for plain text extraction).
|
|
175
|
+
// Decode & last to avoid double-decoding (e.g. &lt; -> < -> <).
|
|
176
|
+
let text = body
|
|
177
|
+
.replace(HTML_TAG_RE, " ")
|
|
178
|
+
.replace(NBSP_RE, " ")
|
|
179
|
+
.replace(LT_RE, "<")
|
|
180
|
+
.replace(GT_RE, ">")
|
|
181
|
+
.replace(QUOT_RE, '"')
|
|
182
|
+
.replace(APOS_RE, "'")
|
|
183
|
+
.replace(AMP_RE, "&")
|
|
184
|
+
.replace(WHITESPACE_RE, " ")
|
|
185
|
+
.trim();
|
|
186
|
+
|
|
187
|
+
if (!text) return undefined;
|
|
188
|
+
|
|
189
|
+
// Truncate to 10,000 chars to avoid exceeding PDS record size limits (~100KB)
|
|
190
|
+
if (text.length > MAX_TEXT_CONTENT_LENGTH) {
|
|
191
|
+
text = text.slice(0, MAX_TEXT_CONTENT_LENGTH - 1) + "\u2026";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return text;
|
|
195
|
+
}
|