|
1 |
+ |
import type { APIRoute } from "astro"; |
|
2 |
+ |
|
|
3 |
+ |
export const prerender = false; |
|
4 |
+ |
|
|
5 |
+ |
const DID = "did:plc:ia2zdnhjaokf5lazhxrmj6eu"; |
|
6 |
+ |
const PDS_URL = "https://polybius.social"; |
|
7 |
+ |
|
|
8 |
+ |
interface DocumentRecord { |
|
9 |
+ |
uri: string; |
|
10 |
+ |
cid: string; |
|
11 |
+ |
value: { |
|
12 |
+ |
$type: string; |
|
13 |
+ |
title: string; |
|
14 |
+ |
site: string; |
|
15 |
+ |
content?: { |
|
16 |
+ |
$type: string; |
|
17 |
+ |
markdown: string; |
|
18 |
+ |
}; |
|
19 |
+ |
textContent?: string; |
|
20 |
+ |
publishedAt: string; |
|
21 |
+ |
}; |
|
22 |
+ |
} |
|
23 |
+ |
|
|
24 |
+ |
interface ListRecordsResponse { |
|
25 |
+ |
records: DocumentRecord[]; |
|
26 |
+ |
cursor?: string; |
|
27 |
+ |
} |
|
28 |
+ |
|
|
29 |
+ |
export const GET: APIRoute = async () => { |
|
30 |
+ |
try { |
|
31 |
+ |
const response = await fetch( |
|
32 |
+ |
`${PDS_URL}/xrpc/com.atproto.repo.listRecords?` + |
|
33 |
+ |
new URLSearchParams({ |
|
34 |
+ |
repo: DID, |
|
35 |
+ |
collection: "site.standard.document", |
|
36 |
+ |
limit: "50", |
|
37 |
+ |
}), |
|
38 |
+ |
); |
|
39 |
+ |
|
|
40 |
+ |
if (!response.ok) { |
|
41 |
+ |
throw new Error(`HTTP error! status: ${response.status}`); |
|
42 |
+ |
} |
|
43 |
+ |
|
|
44 |
+ |
const data = (await response.json()) as ListRecordsResponse; |
|
45 |
+ |
const documents = data.records; |
|
46 |
+ |
|
|
47 |
+ |
// Sort by publishedAt descending |
|
48 |
+ |
documents.sort((a, b) => { |
|
49 |
+ |
const dateA = new Date(a.value.publishedAt); |
|
50 |
+ |
const dateB = new Date(b.value.publishedAt); |
|
51 |
+ |
return dateB.getTime() - dateA.getTime(); |
|
52 |
+ |
}); |
|
53 |
+ |
|
|
54 |
+ |
// Build RSS XML manually to avoid dependencies |
|
55 |
+ |
const items = documents |
|
56 |
+ |
.map((record) => { |
|
57 |
+ |
const doc = record.value; |
|
58 |
+ |
const rkey = record.uri.split("/").pop(); |
|
59 |
+ |
|
|
60 |
+ |
let content = doc.title; |
|
61 |
+ |
let description = doc.title; |
|
62 |
+ |
|
|
63 |
+ |
if (doc.content && doc.content.markdown) { |
|
64 |
+ |
content = doc.content.markdown; |
|
65 |
+ |
description = doc.textContent || doc.title; |
|
66 |
+ |
} else if (doc.textContent) { |
|
67 |
+ |
content = doc.textContent; |
|
68 |
+ |
description = doc.textContent; |
|
69 |
+ |
} |
|
70 |
+ |
|
|
71 |
+ |
// Escape XML entities |
|
72 |
+ |
const escapeXml = (str: string) => |
|
73 |
+ |
str |
|
74 |
+ |
.replace(/&/g, "&") |
|
75 |
+ |
.replace(/</g, "<") |
|
76 |
+ |
.replace(/>/g, ">") |
|
77 |
+ |
.replace(/"/g, """) |
|
78 |
+ |
.replace(/'/g, "'"); |
|
79 |
+ |
|
|
80 |
+ |
const pubDate = new Date(doc.publishedAt).toUTCString(); |
|
81 |
+ |
|
|
82 |
+ |
return ` <item> |
|
83 |
+ |
<title>${escapeXml(doc.title)}</title> |
|
84 |
+ |
<link>https://stevedylan.dev/now/${rkey}</link> |
|
85 |
+ |
<guid>https://stevedylan.dev/now/${rkey}</guid> |
|
86 |
+ |
<description>${escapeXml(description)}</description> |
|
87 |
+ |
<content:encoded><![CDATA[${content}]]></content:encoded> |
|
88 |
+ |
<pubDate>${pubDate}</pubDate> |
|
89 |
+ |
</item>`; |
|
90 |
+ |
}) |
|
91 |
+ |
.join("\n"); |
|
92 |
+ |
|
|
93 |
+ |
const rssXml = `<?xml version="1.0" encoding="UTF-8"?> |
|
94 |
+ |
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"> |
|
95 |
+ |
<channel> |
|
96 |
+ |
<title>Steve Dylan - Updates</title> |
|
97 |
+ |
<description>Small updates from my life that don't quite fit into a blog</description> |
|
98 |
+ |
<link>https://stevedylan.dev/now</link> |
|
99 |
+ |
<atom:link href="https://stevedylan.dev/now/rss.xml" rel="self" type="application/rss+xml"/> |
|
100 |
+ |
<language>en</language> |
|
101 |
+ |
<lastBuildDate>${new Date().toUTCString()}</lastBuildDate> |
|
102 |
+ |
${items} |
|
103 |
+ |
</channel> |
|
104 |
+ |
</rss>`; |
|
105 |
+ |
|
|
106 |
+ |
return new Response(rssXml, { |
|
107 |
+ |
status: 200, |
|
108 |
+ |
headers: { |
|
109 |
+ |
"Content-Type": "application/xml", |
|
110 |
+ |
"Cache-Control": "public, max-age=3600", |
|
111 |
+ |
}, |
|
112 |
+ |
}); |
|
113 |
+ |
} catch (error) { |
|
114 |
+ |
console.error("Error generating RSS feed:", error); |
|
115 |
+ |
|
|
116 |
+ |
// Return an empty feed on error |
|
117 |
+ |
const errorRss = `<?xml version="1.0" encoding="UTF-8"?> |
|
118 |
+ |
<rss version="2.0"> |
|
119 |
+ |
<channel> |
|
120 |
+ |
<title>Steve Dylan - Updates</title> |
|
121 |
+ |
<description>Small updates from my life that don't quite fit into a blog</description> |
|
122 |
+ |
<link>https://stevedylan.dev/now</link> |
|
123 |
+ |
<language>en</language> |
|
124 |
+ |
</channel> |
|
125 |
+ |
</rss>`; |
|
126 |
+ |
|
|
127 |
+ |
return new Response(errorRss, { |
|
128 |
+ |
status: 200, |
|
129 |
+ |
headers: { |
|
130 |
+ |
"Content-Type": "application/xml", |
|
131 |
+ |
}, |
|
132 |
+ |
}); |
|
133 |
+ |
} |
|
134 |
+ |
}; |