@bryanguffey/astro-standard-site 1.0.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,97 @@
1
+ /**
2
+ * Comment aggregation for standard.site documents
3
+ *
4
+ * Fetches replies/comments from various ATProto platforms:
5
+ * - Bluesky posts that link to your document
6
+ * - Direct replies to announcement posts (via bskyPostRef)
7
+ * - Future: Leaflet comments, WhiteWind reactions
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { fetchComments } from 'astro-standard-site/comments';
12
+ *
13
+ * const comments = await fetchComments({
14
+ * documentUri: 'at://did:plc:xxx/site.standard.document/abc123',
15
+ * bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/xyz789',
16
+ * });
17
+ *
18
+ * // comments is an array of unified Comment objects
19
+ * ```
20
+ */
21
+ export interface Author {
22
+ did: string;
23
+ handle: string;
24
+ displayName?: string;
25
+ avatar?: string;
26
+ }
27
+ export interface Comment {
28
+ /** Unique identifier (AT-URI) */
29
+ uri: string;
30
+ /** Content ID */
31
+ cid: string;
32
+ /** Comment text */
33
+ text: string;
34
+ /** Author information */
35
+ author: Author;
36
+ /** When the comment was created */
37
+ createdAt: Date;
38
+ /** Source platform */
39
+ source: 'bluesky' | 'leaflet' | 'whitewind' | 'unknown';
40
+ /** URL to view the comment on its platform */
41
+ sourceUrl: string;
42
+ /** Parent comment URI (for threaded replies) */
43
+ parentUri?: string;
44
+ /** Like count (if available) */
45
+ likeCount?: number;
46
+ /** Reply count (if available) */
47
+ replyCount?: number;
48
+ /** Nested replies */
49
+ replies?: Comment[];
50
+ }
51
+ export interface FetchCommentsOptions {
52
+ /** AT-URI of the site.standard.document record */
53
+ documentUri?: string;
54
+ /** AT-URI of a Bluesky post that announces/links the document */
55
+ bskyPostUri?: string;
56
+ /** Canonical URL of the blog post (for finding mentions) */
57
+ canonicalUrl?: string;
58
+ /** Maximum depth for nested replies */
59
+ maxDepth?: number;
60
+ /** Maximum total comments to fetch */
61
+ maxComments?: number;
62
+ /** PDS service for API calls */
63
+ service?: string;
64
+ }
65
+ /**
66
+ * Fetch replies to a Bluesky post
67
+ */
68
+ export declare function fetchBlueskyReplies(postUri: string, options?: {
69
+ maxDepth?: number;
70
+ service?: string;
71
+ }): Promise<Comment[]>;
72
+ /**
73
+ * Search Bluesky for posts mentioning a URL
74
+ * Useful for finding discussions about your blog post
75
+ */
76
+ export declare function searchBlueskyMentions(url: string, options?: {
77
+ maxResults?: number;
78
+ service?: string;
79
+ }): Promise<Comment[]>;
80
+ /**
81
+ * Build a tree structure from flat comments
82
+ */
83
+ export declare function buildCommentTree(comments: Comment[]): Comment[];
84
+ /**
85
+ * Fetch all comments for a document from multiple sources
86
+ */
87
+ export declare function fetchComments(options: FetchCommentsOptions): Promise<Comment[]>;
88
+ /**
89
+ * Flatten a comment tree back to an array (for simple list display)
90
+ */
91
+ export declare function flattenComments(tree: Comment[]): Comment[];
92
+ /**
93
+ * Get comment count (including nested replies)
94
+ */
95
+ export declare function countComments(tree: Comment[]): number;
96
+ export type { FetchCommentsOptions as CommentFetchOptions };
97
+ //# sourceMappingURL=comments.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.d.ts","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAIH,MAAM,WAAW,MAAM;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,OAAO;IACtB,iCAAiC;IACjC,GAAG,EAAE,MAAM,CAAC;IACZ,iBAAiB;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,mBAAmB;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,yBAAyB;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,mCAAmC;IACnC,SAAS,EAAE,IAAI,CAAC;IAChB,sBAAsB;IACtB,MAAM,EAAE,SAAS,GAAG,SAAS,GAAG,WAAW,GAAG,SAAS,CAAC;IACxD,8CAA8C;IAC9C,SAAS,EAAE,MAAM,CAAC;IAClB,gDAAgD;IAChD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gCAAgC;IAChC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iCAAiC;IACjC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,qBAAqB;IACrB,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iEAAiE;IACjE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,4DAA4D;IAC5D,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,uCAAuC;IACvC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,sCAAsC;IACtC,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AA6DD;;GAEG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IACP,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;CACb,GACL,OAAO,CAAC,OAAO,EAAE,CAAC,CAuBpB;AAED;;;GAGG;AACH,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,MAAM,EACX,OAAO,GAAE;IACP,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;CACb,GACL,OAAO,CAAC,OAAO,EAAE,CAAC,CAsBpB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAsC/D;AAED;;GAEG;AACH,wBAAsB,aAAa,CACjC,OAAO,EAAE,oBAAoB,GAC5B,OAAO,CAAC,OAAO,EAAE,CAAC,CA4CpB;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,EAAE,CAc1D;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,MAAM,CAcrD;AAED,YAAY,EAAE,oBAAoB,IAAI,mBAAmB,EAAE,CAAC"}
@@ -0,0 +1,221 @@
1
+ /**
2
+ * Comment aggregation for standard.site documents
3
+ *
4
+ * Fetches replies/comments from various ATProto platforms:
5
+ * - Bluesky posts that link to your document
6
+ * - Direct replies to announcement posts (via bskyPostRef)
7
+ * - Future: Leaflet comments, WhiteWind reactions
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { fetchComments } from 'astro-standard-site/comments';
12
+ *
13
+ * const comments = await fetchComments({
14
+ * documentUri: 'at://did:plc:xxx/site.standard.document/abc123',
15
+ * bskyPostUri: 'at://did:plc:xxx/app.bsky.feed.post/xyz789',
16
+ * });
17
+ *
18
+ * // comments is an array of unified Comment objects
19
+ * ```
20
+ */
21
+ import { AtpAgent } from '@atproto/api';
22
+ /**
23
+ * Convert a Bluesky post to our unified Comment format
24
+ */
25
+ function bskyPostToComment(post, depth = 0) {
26
+ const author = {
27
+ did: post.author.did,
28
+ handle: post.author.handle,
29
+ displayName: post.author.displayName,
30
+ avatar: post.author.avatar,
31
+ };
32
+ // Build the Bluesky URL
33
+ const postId = post.uri.split('/').pop();
34
+ const sourceUrl = `https://bsky.app/profile/${post.author.handle}/post/${postId}`;
35
+ const comment = {
36
+ uri: post.uri,
37
+ cid: post.cid,
38
+ text: post.record?.text || '',
39
+ author,
40
+ createdAt: new Date(post.record?.createdAt || post.indexedAt),
41
+ source: 'bluesky',
42
+ sourceUrl,
43
+ likeCount: post.likeCount,
44
+ replyCount: post.replyCount,
45
+ };
46
+ // Extract parent if this is a reply
47
+ if (post.record?.reply?.parent?.uri) {
48
+ comment.parentUri = post.record.reply.parent.uri;
49
+ }
50
+ return comment;
51
+ }
52
+ /**
53
+ * Recursively process a thread tree from Bluesky
54
+ */
55
+ function processThread(thread, maxDepth, currentDepth = 0) {
56
+ const comments = [];
57
+ if (!thread || currentDepth > maxDepth)
58
+ return comments;
59
+ // Process the current post if it's a reply (not the root)
60
+ if (thread.post && currentDepth > 0) {
61
+ comments.push(bskyPostToComment(thread.post, currentDepth));
62
+ }
63
+ // Process replies
64
+ if (thread.replies && Array.isArray(thread.replies)) {
65
+ for (const reply of thread.replies) {
66
+ const replyComments = processThread(reply, maxDepth, currentDepth + 1);
67
+ comments.push(...replyComments);
68
+ }
69
+ }
70
+ return comments;
71
+ }
72
+ /**
73
+ * Fetch replies to a Bluesky post
74
+ */
75
+ export async function fetchBlueskyReplies(postUri, options = {}) {
76
+ const { maxDepth = 3, service = 'https://public.api.bsky.app' } = options;
77
+ const agent = new AtpAgent({ service });
78
+ try {
79
+ const response = await agent.api.app.bsky.feed.getPostThread({
80
+ uri: postUri,
81
+ depth: maxDepth,
82
+ });
83
+ if (!response.data.thread || response.data.thread.$type === 'app.bsky.feed.defs#notFoundPost') {
84
+ return [];
85
+ }
86
+ // Process the thread, skipping the root post (we only want replies)
87
+ const comments = processThread(response.data.thread, maxDepth, 0);
88
+ return comments;
89
+ }
90
+ catch (error) {
91
+ console.error('Failed to fetch Bluesky replies:', error);
92
+ return [];
93
+ }
94
+ }
95
+ /**
96
+ * Search Bluesky for posts mentioning a URL
97
+ * Useful for finding discussions about your blog post
98
+ */
99
+ export async function searchBlueskyMentions(url, options = {}) {
100
+ const { maxResults = 25, service = 'https://public.api.bsky.app' } = options;
101
+ const agent = new AtpAgent({ service });
102
+ try {
103
+ // Search for posts containing the URL
104
+ const response = await agent.api.app.bsky.feed.searchPosts({
105
+ q: url,
106
+ limit: maxResults,
107
+ });
108
+ if (!response.data.posts) {
109
+ return [];
110
+ }
111
+ // Convert to our Comment format
112
+ return response.data.posts.map(post => bskyPostToComment(post));
113
+ }
114
+ catch (error) {
115
+ console.error('Failed to search Bluesky mentions:', error);
116
+ return [];
117
+ }
118
+ }
119
+ /**
120
+ * Build a tree structure from flat comments
121
+ */
122
+ export function buildCommentTree(comments) {
123
+ const commentMap = new Map();
124
+ const rootComments = [];
125
+ // First pass: index all comments
126
+ for (const comment of comments) {
127
+ commentMap.set(comment.uri, { ...comment, replies: [] });
128
+ }
129
+ // Second pass: build tree
130
+ for (const comment of comments) {
131
+ const node = commentMap.get(comment.uri);
132
+ if (comment.parentUri && commentMap.has(comment.parentUri)) {
133
+ const parent = commentMap.get(comment.parentUri);
134
+ parent.replies = parent.replies || [];
135
+ parent.replies.push(node);
136
+ }
137
+ else {
138
+ rootComments.push(node);
139
+ }
140
+ }
141
+ // Sort by date (oldest first for conversations)
142
+ const sortByDate = (a, b) => a.createdAt.getTime() - b.createdAt.getTime();
143
+ const sortTree = (comments) => {
144
+ comments.sort(sortByDate);
145
+ for (const comment of comments) {
146
+ if (comment.replies?.length) {
147
+ sortTree(comment.replies);
148
+ }
149
+ }
150
+ };
151
+ sortTree(rootComments);
152
+ return rootComments;
153
+ }
154
+ /**
155
+ * Fetch all comments for a document from multiple sources
156
+ */
157
+ export async function fetchComments(options) {
158
+ const { bskyPostUri, canonicalUrl, maxDepth = 3, maxComments = 100, service = 'https://public.api.bsky.app', } = options;
159
+ const allComments = [];
160
+ // Fetch replies to the announcement post
161
+ if (bskyPostUri) {
162
+ const replies = await fetchBlueskyReplies(bskyPostUri, { maxDepth, service });
163
+ allComments.push(...replies);
164
+ }
165
+ // Search for mentions of the canonical URL
166
+ if (canonicalUrl) {
167
+ const mentions = await searchBlueskyMentions(canonicalUrl, {
168
+ maxResults: Math.max(10, maxComments - allComments.length),
169
+ service,
170
+ });
171
+ // Filter out duplicates and the original post
172
+ const existingUris = new Set(allComments.map(c => c.uri));
173
+ if (bskyPostUri)
174
+ existingUris.add(bskyPostUri);
175
+ for (const mention of mentions) {
176
+ if (!existingUris.has(mention.uri)) {
177
+ allComments.push(mention);
178
+ existingUris.add(mention.uri);
179
+ }
180
+ }
181
+ }
182
+ // Future: Add Leaflet comment fetching
183
+ // Future: Add WhiteWind reaction fetching
184
+ // Limit total comments
185
+ const limitedComments = allComments.slice(0, maxComments);
186
+ // Build into a tree structure
187
+ return buildCommentTree(limitedComments);
188
+ }
189
+ /**
190
+ * Flatten a comment tree back to an array (for simple list display)
191
+ */
192
+ export function flattenComments(tree) {
193
+ const flat = [];
194
+ const walk = (comments, depth = 0) => {
195
+ for (const comment of comments) {
196
+ flat.push({ ...comment, replies: undefined });
197
+ if (comment.replies?.length) {
198
+ walk(comment.replies, depth + 1);
199
+ }
200
+ }
201
+ };
202
+ walk(tree);
203
+ return flat;
204
+ }
205
+ /**
206
+ * Get comment count (including nested replies)
207
+ */
208
+ export function countComments(tree) {
209
+ let count = 0;
210
+ const walk = (comments) => {
211
+ for (const comment of comments) {
212
+ count++;
213
+ if (comment.replies?.length) {
214
+ walk(comment.replies);
215
+ }
216
+ }
217
+ };
218
+ walk(tree);
219
+ return count;
220
+ }
221
+ //# sourceMappingURL=comments.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"comments.js","sourceRoot":"","sources":["../src/comments.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Content transformation utilities for standard.site
3
+ *
4
+ * Converts Astro blog post content into formats suitable for ATProto:
5
+ * - Strips markdown to plain text for `textContent` (search/indexing)
6
+ * - Converts custom sidenotes to standard markdown
7
+ * - Resolves relative links to absolute URLs
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { transformContent, stripToPlainText } from 'astro-standard-site/content';
12
+ *
13
+ * const result = transformContent(markdown, {
14
+ * siteUrl: 'https://bryanguffey.com',
15
+ * postPath: '/blog/my-post',
16
+ * });
17
+ *
18
+ * // result.markdown - cleaned markdown for ATProto
19
+ * // result.textContent - plain text for search
20
+ * ```
21
+ */
22
+ export interface TransformOptions {
23
+ /** Base URL of your site (e.g., 'https://bryanguffey.com') */
24
+ siteUrl: string;
25
+ /** Path to the current post (e.g., '/blog/my-post') */
26
+ postPath?: string;
27
+ }
28
+ export interface TransformResult {
29
+ /** Transformed markdown suitable for ATProto */
30
+ markdown: string;
31
+ /** Plain text version for textContent field */
32
+ textContent: string;
33
+ /** Word count */
34
+ wordCount: number;
35
+ /** Estimated reading time in minutes */
36
+ readingTime: number;
37
+ }
38
+ /**
39
+ * Convert HTML sidenotes to markdown blockquotes
40
+ *
41
+ * Transforms:
42
+ * ```html
43
+ * <div class="sidenote sidenote--tip">
44
+ * <span class="sidenote-label">Tip</span>
45
+ * <p>Content here</p>
46
+ * </div>
47
+ * ```
48
+ *
49
+ * Into:
50
+ * ```markdown
51
+ * > **Tip:** Content here
52
+ * ```
53
+ */
54
+ export declare function convertSidenotes(markdown: string): string;
55
+ /**
56
+ * Convert HTML sidenotes (multi-paragraph) to markdown
57
+ * Handles more complex sidenote structures
58
+ */
59
+ export declare function convertComplexSidenotes(markdown: string): string;
60
+ /**
61
+ * Resolve relative URLs to absolute URLs
62
+ *
63
+ * Converts:
64
+ * - `[Link](/page)` → `[Link](https://example.com/page)`
65
+ * - `![Image](/img.png)` → `![Image](https://example.com/img.png)`
66
+ */
67
+ export declare function resolveRelativeLinks(markdown: string, siteUrl: string): string;
68
+ /**
69
+ * Strip markdown to plain text for indexing/search
70
+ *
71
+ * Removes:
72
+ * - Markdown formatting (**, *, #, etc.)
73
+ * - Links (keeps link text)
74
+ * - Images
75
+ * - Code blocks
76
+ * - HTML tags
77
+ */
78
+ export declare function stripToPlainText(markdown: string): string;
79
+ /**
80
+ * Calculate word count from text
81
+ */
82
+ export declare function countWords(text: string): number;
83
+ /**
84
+ * Calculate reading time in minutes (assuming 200 words per minute)
85
+ */
86
+ export declare function calculateReadingTime(wordCount: number, wordsPerMinute?: number): number;
87
+ /**
88
+ * Full content transformation pipeline
89
+ *
90
+ * Takes raw markdown (with HTML sidenotes) and produces:
91
+ * - Clean markdown suitable for ATProto platforms
92
+ * - Plain text for search/indexing
93
+ * - Metadata (word count, reading time)
94
+ */
95
+ export declare function transformContent(rawMarkdown: string, options: TransformOptions): TransformResult;
96
+ /**
97
+ * Transform an Astro blog post entry for standard.site
98
+ *
99
+ * @example
100
+ * ```ts
101
+ * import { getCollection } from 'astro:content';
102
+ * import { transformPost } from 'astro-standard-site/content';
103
+ *
104
+ * const posts = await getCollection('blog');
105
+ * for (const post of posts) {
106
+ * const standardDoc = transformPost(post, {
107
+ * siteUrl: 'https://bryanguffey.com',
108
+ * });
109
+ * await publisher.publishDocument(standardDoc);
110
+ * }
111
+ * ```
112
+ */
113
+ export interface AstroBlogPost {
114
+ slug: string;
115
+ body: string;
116
+ data: {
117
+ title: string;
118
+ description?: string;
119
+ date: Date;
120
+ tags?: string[];
121
+ draft?: boolean;
122
+ };
123
+ }
124
+ export interface StandardSiteDocumentInput {
125
+ site: string;
126
+ title: string;
127
+ publishedAt: string;
128
+ path?: string;
129
+ description?: string;
130
+ tags?: string[];
131
+ textContent?: string;
132
+ content?: {
133
+ $type: string;
134
+ text: string;
135
+ version?: string;
136
+ };
137
+ }
138
+ export declare function transformPost(post: AstroBlogPost, options: {
139
+ siteUrl: string;
140
+ }): StandardSiteDocumentInput;
141
+ //# sourceMappingURL=content.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"content.d.ts","sourceRoot":"","sources":["../src/content.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,8DAA8D;IAC9D,OAAO,EAAE,MAAM,CAAC;IAChB,uDAAuD;IACvD,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,eAAe;IAC9B,gDAAgD;IAChD,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYzD;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAqChE;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAY9E;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CA0CzD;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED;;GAEG;AACH,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,cAAc,SAAM,GAAG,MAAM,CAEpF;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,gBAAgB,GACxB,eAAe,CAuBjB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE;QACJ,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,IAAI,EAAE,IAAI,CAAC;QACX,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAChB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC;CACH;AAED,MAAM,WAAW,yBAAyB;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE;QACR,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;CACH;AAED,wBAAgB,aAAa,CAC3B,IAAI,EAAE,aAAa,EACnB,OAAO,EAAE;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAC3B,yBAAyB,CAuB3B"}
@@ -0,0 +1,202 @@
1
+ /**
2
+ * Content transformation utilities for standard.site
3
+ *
4
+ * Converts Astro blog post content into formats suitable for ATProto:
5
+ * - Strips markdown to plain text for `textContent` (search/indexing)
6
+ * - Converts custom sidenotes to standard markdown
7
+ * - Resolves relative links to absolute URLs
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { transformContent, stripToPlainText } from 'astro-standard-site/content';
12
+ *
13
+ * const result = transformContent(markdown, {
14
+ * siteUrl: 'https://bryanguffey.com',
15
+ * postPath: '/blog/my-post',
16
+ * });
17
+ *
18
+ * // result.markdown - cleaned markdown for ATProto
19
+ * // result.textContent - plain text for search
20
+ * ```
21
+ */
22
+ /**
23
+ * Convert HTML sidenotes to markdown blockquotes
24
+ *
25
+ * Transforms:
26
+ * ```html
27
+ * <div class="sidenote sidenote--tip">
28
+ * <span class="sidenote-label">Tip</span>
29
+ * <p>Content here</p>
30
+ * </div>
31
+ * ```
32
+ *
33
+ * Into:
34
+ * ```markdown
35
+ * > **Tip:** Content here
36
+ * ```
37
+ */
38
+ export function convertSidenotes(markdown) {
39
+ // Match sidenote divs with various formats
40
+ const sidenoteRegex = /<div\s+class="sidenote(?:\s+sidenote--(warning|tip))?">\s*<span\s+class="sidenote-label">([^<]+)<\/span>\s*<p>([^<]+)<\/p>\s*<\/div>/gi;
41
+ return markdown.replace(sidenoteRegex, (_, type, label, content) => {
42
+ // Clean up the content
43
+ const cleanContent = content.trim();
44
+ const cleanLabel = label.trim();
45
+ // Convert to blockquote with label
46
+ return `\n> **${cleanLabel}:** ${cleanContent}\n`;
47
+ });
48
+ }
49
+ /**
50
+ * Convert HTML sidenotes (multi-paragraph) to markdown
51
+ * Handles more complex sidenote structures
52
+ */
53
+ export function convertComplexSidenotes(markdown) {
54
+ // First pass: simple sidenotes
55
+ let result = convertSidenotes(markdown);
56
+ // Second pass: sidenotes with multiple paragraphs or nested content
57
+ const complexSidenoteRegex = /<div\s+class="sidenote[^"]*">([\s\S]*?)<\/div>/gi;
58
+ result = result.replace(complexSidenoteRegex, (match, innerContent) => {
59
+ // Extract label if present
60
+ const labelMatch = innerContent.match(/<span\s+class="sidenote-label">([^<]+)<\/span>/i);
61
+ const label = labelMatch ? labelMatch[1].trim() : 'Note';
62
+ // Remove the label span
63
+ let content = innerContent.replace(/<span\s+class="sidenote-label">[^<]+<\/span>/gi, '');
64
+ // Convert remaining HTML to plain text with basic formatting
65
+ content = content
66
+ .replace(/<p>/gi, '')
67
+ .replace(/<\/p>/gi, '\n')
68
+ .replace(/<strong>/gi, '**')
69
+ .replace(/<\/strong>/gi, '**')
70
+ .replace(/<em>/gi, '*')
71
+ .replace(/<\/em>/gi, '*')
72
+ .replace(/<a\s+href="([^"]+)"[^>]*>([^<]+)<\/a>/gi, '[$2]($1)')
73
+ .replace(/<[^>]+>/g, '') // Remove any remaining HTML tags
74
+ .trim();
75
+ // Format as blockquote
76
+ const lines = content.split('\n').filter((line) => line.trim());
77
+ const quotedLines = lines.map((line, i) => i === 0 ? `> **${label}:** ${line}` : `> ${line}`);
78
+ return '\n' + quotedLines.join('\n') + '\n';
79
+ });
80
+ return result;
81
+ }
82
+ /**
83
+ * Resolve relative URLs to absolute URLs
84
+ *
85
+ * Converts:
86
+ * - `[Link](/page)` → `[Link](https://example.com/page)`
87
+ * - `![Image](/img.png)` → `![Image](https://example.com/img.png)`
88
+ */
89
+ export function resolveRelativeLinks(markdown, siteUrl) {
90
+ const baseUrl = siteUrl.replace(/\/$/, '');
91
+ // Match markdown links and images with relative URLs
92
+ // [text](/path) or ![alt](/path)
93
+ const linkRegex = /(!?\[[^\]]*\])\((?!https?:\/\/|mailto:|#)([^)]+)\)/g;
94
+ return markdown.replace(linkRegex, (_, prefix, path) => {
95
+ // Ensure path starts with /
96
+ const absolutePath = path.startsWith('/') ? path : `/${path}`;
97
+ return `${prefix}(${baseUrl}${absolutePath})`;
98
+ });
99
+ }
100
+ /**
101
+ * Strip markdown to plain text for indexing/search
102
+ *
103
+ * Removes:
104
+ * - Markdown formatting (**, *, #, etc.)
105
+ * - Links (keeps link text)
106
+ * - Images
107
+ * - Code blocks
108
+ * - HTML tags
109
+ */
110
+ export function stripToPlainText(markdown) {
111
+ let text = markdown;
112
+ // Remove code blocks first (preserve nothing)
113
+ text = text.replace(/```[\s\S]*?```/g, '');
114
+ text = text.replace(/`[^`]+`/g, '');
115
+ // Remove images
116
+ text = text.replace(/!\[[^\]]*\]\([^)]+\)/g, '');
117
+ // Convert links to just their text
118
+ text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
119
+ // Remove HTML tags
120
+ text = text.replace(/<[^>]+>/g, '');
121
+ // Remove heading markers
122
+ text = text.replace(/^#{1,6}\s+/gm, '');
123
+ // Remove bold/italic markers
124
+ text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
125
+ text = text.replace(/\*([^*]+)\*/g, '$1');
126
+ text = text.replace(/__([^_]+)__/g, '$1');
127
+ text = text.replace(/_([^_]+)_/g, '$1');
128
+ // Remove blockquote markers
129
+ text = text.replace(/^>\s*/gm, '');
130
+ // Remove list markers
131
+ text = text.replace(/^[\s]*[-*+]\s+/gm, '');
132
+ text = text.replace(/^[\s]*\d+\.\s+/gm, '');
133
+ // Remove horizontal rules
134
+ text = text.replace(/^[-*_]{3,}$/gm, '');
135
+ // Collapse multiple newlines
136
+ text = text.replace(/\n{3,}/g, '\n\n');
137
+ // Trim whitespace
138
+ text = text.trim();
139
+ return text;
140
+ }
141
+ /**
142
+ * Calculate word count from text
143
+ */
144
+ export function countWords(text) {
145
+ return text.split(/\s+/).filter(word => word.length > 0).length;
146
+ }
147
+ /**
148
+ * Calculate reading time in minutes (assuming 200 words per minute)
149
+ */
150
+ export function calculateReadingTime(wordCount, wordsPerMinute = 200) {
151
+ return Math.max(1, Math.ceil(wordCount / wordsPerMinute));
152
+ }
153
+ /**
154
+ * Full content transformation pipeline
155
+ *
156
+ * Takes raw markdown (with HTML sidenotes) and produces:
157
+ * - Clean markdown suitable for ATProto platforms
158
+ * - Plain text for search/indexing
159
+ * - Metadata (word count, reading time)
160
+ */
161
+ export function transformContent(rawMarkdown, options) {
162
+ // Step 1: Convert sidenotes from HTML to markdown blockquotes
163
+ let markdown = convertComplexSidenotes(rawMarkdown);
164
+ // Step 2: Resolve relative links to absolute URLs
165
+ markdown = resolveRelativeLinks(markdown, options.siteUrl);
166
+ // Step 3: Clean up extra whitespace
167
+ markdown = markdown.replace(/\n{3,}/g, '\n\n').trim();
168
+ // Step 4: Generate plain text version
169
+ const textContent = stripToPlainText(markdown);
170
+ // Step 5: Calculate metadata
171
+ const wordCount = countWords(textContent);
172
+ const readingTime = calculateReadingTime(wordCount);
173
+ return {
174
+ markdown,
175
+ textContent,
176
+ wordCount,
177
+ readingTime,
178
+ };
179
+ }
180
+ export function transformPost(post, options) {
181
+ const postPath = `/blog/${post.slug}`;
182
+ // Transform the content
183
+ const transformed = transformContent(post.body, {
184
+ siteUrl: options.siteUrl,
185
+ postPath,
186
+ });
187
+ return {
188
+ site: options.siteUrl,
189
+ title: post.data.title,
190
+ publishedAt: post.data.date.toISOString(),
191
+ path: postPath,
192
+ description: post.data.description,
193
+ tags: post.data.tags,
194
+ textContent: transformed.textContent,
195
+ content: {
196
+ $type: 'site.standard.content.markdown',
197
+ text: transformed.markdown,
198
+ version: '1.0',
199
+ },
200
+ };
201
+ }
202
+ //# sourceMappingURL=content.js.map