chore: moved rss into astro 5d13a158
Steve · 2026-01-08 19:56 3 file(s) · +137 −3
packages/client/src/pages/now/index.astro +1 −1
24 24
    <h2 class="text-xl font-semibold">Updates</h2>
25 25
    <a
26 26
				class="inline-block p-2 sm:hover:text-link"
27 -
				href="https://api.stevedylan.dev/now/rss"
27 +
				href="/now/rss.xml"
28 28
				target="_blank"
29 29
				rel="noopener noreferrer"
30 30
			>
packages/client/src/pages/now/rss.xml.ts (added) +134 −0
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, "&amp;")
75 +
						.replace(/</g, "&lt;")
76 +
						.replace(/>/g, "&gt;")
77 +
						.replace(/"/g, "&quot;")
78 +
						.replace(/'/g, "&apos;");
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 +
};
packages/server/src/routes/now.ts +2 −2
233 233
234 234
			feed.addItem({
235 235
				title: doc.title,
236 -
				id: `https://stevedylan.dev/pds?rkey=${rkey}`,
237 -
				link: doc.site,
236 +
				id: `https://stevedylan.dev/now/${rkey}`,
237 +
				link: `https://stevedylan.dev/now/${rkey}`,
238 238
				description: description,
239 239
				content: content,
240 240
				date: new Date(doc.publishedAt),