From 230bf1af78e51f583ce93323e2502eada7d511ff Mon Sep 17 00:00:00 2001
From: lemma <>
Date: Sun, 7 Jun 2026 15:36:26 -0500
Subject: [PATCH] Add support for Atom feeds
---
infra/api/src/index.ts | 97 ++++++++++++++++++++++++++++++++++++++++
infra/cf/cloudfront.yaml | 12 +++++
2 files changed, 109 insertions(+)
diff --git a/infra/api/src/index.ts b/infra/api/src/index.ts
index e69a980..d4d062f 100644
--- a/infra/api/src/index.ts
+++ b/infra/api/src/index.ts
@@ -209,6 +209,102 @@ async function verifyPublication(rawPath: string): Promise/g, '>').replace(/"/g, '"');
+}
+
+async function getPublicationFeed(rawPath: string): Promise {
+ if (!SITE_DOMAIN) {
+ return { statusCode: 404, headers: { 'Content-Type': 'text/plain' }, body: 'Not found' };
+ }
+
+ const feedSuffix = '/feed.xml';
+ const rawPubPath = rawPath.slice(0, rawPath.length - feedSuffix.length);
+ const pubPath = decodeURIComponent(rawPubPath);
+ const pubUrl = `https://${SITE_DOMAIN}${pubPath}`;
+
+ const urlLookup = await ddb.send(new GetCommand({
+ TableName: TABLE,
+ Key: { PK: `url#${pubUrl}`, SK: 'info' },
+ }));
+ if (!urlLookup.Item?.pubAtUri) {
+ return { statusCode: 404, headers: { 'Content-Type': 'text/plain' }, body: 'Not found' };
+ }
+
+ const pubAtUri = urlLookup.Item.pubAtUri as string;
+ const atParts = pubAtUri.slice('at://'.length).split('/');
+ const did = atParts[0];
+ const pubRkey = atParts[2];
+
+ const [pubResult, postsResult, authorHandle] = await Promise.all([
+ ddb.send(new GetCommand({ TableName: TABLE, Key: { PK: `u#${did}#pub`, SK: `pub#${pubRkey}` } })),
+ ddb.send(new QueryCommand({
+ TableName: TABLE,
+ KeyConditionExpression: 'PK = :pk',
+ FilterExpression: 'visibility = :vis AND site = :site',
+ ExpressionAttributeValues: { ':pk': `u#${did}#post`, ':vis': 'published', ':site': pubAtUri },
+ ScanIndexForward: false,
+ Limit: 100,
+ })),
+ resolveAuthorHandle(did),
+ ]);
+
+ if (!pubResult.Item) {
+ return { statusCode: 404, headers: { 'Content-Type': 'text/plain' }, body: 'Not found' };
+ }
+
+ const pub = pubResult.Item;
+ const pubName = pub.name as string;
+ const pubSiteUrl = (pub.url as string).replace(/\/$/, '');
+ const pubDescription = pub.description as string | undefined;
+ const posts = (postsResult.Items ?? []).slice(0, 20);
+ const selfUrl = `https://${SITE_DOMAIN}${rawPath}`;
+ const updatedAt = posts.length > 0
+ ? ((posts[0].updatedAt ?? posts[0].publishedAt) as string)
+ : new Date().toISOString();
+
+ const entryXml = posts.map((post) => {
+ const base = (post.resolvedUrl as string | undefined ?? pubSiteUrl).replace(/\/$/, '');
+ const postUrl = `${base}${post.path as string | undefined ?? ''}`;
+ const summary = (post.description ?? post.textContent) as string | undefined;
+ const lines = [
+ ' ',
+ ` ${escXml(post.atUri as string)}`,
+ ` ${escXml(post.title as string)}`,
+ ` `,
+ ` ${post.publishedAt as string}`,
+ ` ${(post.updatedAt ?? post.publishedAt) as string}`,
+ ];
+ if (summary) lines.push(` ${escXml(summary)}`);
+ lines.push(' ');
+ return lines.join('\n');
+ });
+
+ const feedLines = [
+ '',
+ '',
+ ` ${escXml(pubAtUri)}`,
+ ` ${escXml(pubName)}`,
+ ];
+ if (pubDescription) feedLines.push(` ${escXml(pubDescription)}`);
+ feedLines.push(
+ ` `,
+ ` `,
+ ` ${updatedAt}`,
+ ' ',
+ ` ${escXml(authorHandle)}`,
+ ' ',
+ ...entryXml,
+ '',
+ );
+
+ return {
+ statusCode: 200,
+ headers: { 'Content-Type': 'application/atom+xml; charset=utf-8', 'Cache-Control': 'public, max-age=300' },
+ body: feedLines.join('\n'),
+ };
+}
+
async function resolvePublicationMeta(atUri: string): Promise<{ url: string; name: string }> {
const parts = atUri.slice('at://'.length).split('/')
const pubDid = parts[0]
@@ -886,6 +982,7 @@ export async function handler(event: APIGatewayProxyEventV2): Promise