From 4428f35dd47d03cdea43605d92dc02af19a66b90 Mon Sep 17 00:00:00 2001
From: Julien Herr
Date: Fri, 22 May 2026 18:41:21 +0200
Subject: [PATCH] fix(feed): add Atom link in emails page, fix HTML stripping,
use request URL for self-link
- Add Atom Feed URL to the Feed Details card in the emails page
- Fix extractBodyContent to handle emails without a closing
)
- Use the actual request URL origin for atom:link rel="self" in RSS/Atom
feeds, guaranteeing it always matches the document location regardless
of how DOMAIN is configured
Co-Authored-By: Claude Sonnet 4.6
---
src/routes/admin/emails.tsx | 3 ++-
src/routes/atom.ts | 2 ++
src/routes/rss.ts | 2 ++
src/utils/feed-generator.ts | 31 +++++++++++++++++++++++++------
4 files changed, 31 insertions(+), 7 deletions(-)
diff --git a/src/routes/admin/emails.tsx b/src/routes/admin/emails.tsx
index b1fbb94..f182d74 100644
--- a/src/routes/admin/emails.tsx
+++ b/src/routes/admin/emails.tsx
@@ -93,7 +93,7 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
const emailAddress = `${feedId}@${env.DOMAIN}`;
const rssUrl = `https://${env.DOMAIN}/rss/${feedId}`;
-
+ const atomUrl = `https://${env.DOMAIN}/atom/${feedId}`;
return c.html(
@@ -114,6 +114,7 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
+
diff --git a/src/routes/atom.ts b/src/routes/atom.ts
index f0ae451..dcde4be 100644
--- a/src/routes/atom.ts
+++ b/src/routes/atom.ts
@@ -16,11 +16,13 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise {
}
const baseUrl = `https://${c.env.DOMAIN}`;
+ const selfUrl = new URL(c.req.url).origin + `/atom/${feedId}`;
const atomXml = generateAtomFeed(
feedData.feedConfig,
feedData.emails,
baseUrl,
feedId,
+ selfUrl,
);
const linkHeader = [
`<${baseUrl}/hub>; rel="hub"`,
diff --git a/src/routes/rss.ts b/src/routes/rss.ts
index 12ed6ee..87a0acf 100644
--- a/src/routes/rss.ts
+++ b/src/routes/rss.ts
@@ -16,11 +16,13 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise {
}
const baseUrl = `https://${c.env.DOMAIN}`;
+ const selfUrl = new URL(c.req.url).origin + `/rss/${feedId}`;
const rssXml = generateRssFeed(
feedData.feedConfig,
feedData.emails,
baseUrl,
feedId,
+ selfUrl,
);
const linkHeader = [
`<${baseUrl}/hub>; rel="hub"`,
diff --git a/src/utils/feed-generator.ts b/src/utils/feed-generator.ts
index a4d466e..e6c8ef9 100644
--- a/src/utils/feed-generator.ts
+++ b/src/utils/feed-generator.ts
@@ -16,8 +16,12 @@ function parseFromAddress(from: string): { name: string; email?: string } {
// Email content is stored as a full HTML document. Feed readers expect only
// the body fragment in /, not a full document.
export function extractBodyContent(html: string): string {
- const match = html.match(/]*>([\s\S]*?)<\/body>/i);
- return match ? match[1] : html;
+ const withClose = html.match(/]*>([\s\S]*?)<\/body>/i);
+ if (withClose) return withClose[1];
+ // Some HTML emails omit ; capture everything after the opening tag
+ const withoutClose = html.match(/]*>([\s\S]*)/i);
+ if (withoutClose) return withoutClose[1].replace(/<\/html>\s*$/i, "");
+ return html;
}
function buildFeed(
@@ -25,6 +29,7 @@ function buildFeed(
emails: EmailData[],
baseUrl: string,
feedId: string,
+ selfUrl?: { rss?: string; atom?: string },
): Feed {
const feed = new Feed({
title: feedConfig.title,
@@ -39,8 +44,8 @@ function buildFeed(
generator: "kill-the-news",
copyright: `Copyright © ${new Date().getFullYear()} ${feedConfig.title}`,
feedLinks: {
- rss: `${baseUrl}/rss/${feedId}`,
- atom: `${baseUrl}/atom/${feedId}`,
+ rss: selfUrl?.rss ?? `${baseUrl}/rss/${feedId}`,
+ atom: selfUrl?.atom ?? `${baseUrl}/atom/${feedId}`,
},
author: feedConfig.author
? {
@@ -80,8 +85,15 @@ export function generateRssFeed(
emails: EmailData[],
baseUrl: string,
feedId: string,
+ selfUrl?: string,
): string {
- return buildFeed(feedConfig, emails, baseUrl, feedId).rss2();
+ return buildFeed(
+ feedConfig,
+ emails,
+ baseUrl,
+ feedId,
+ selfUrl ? { rss: selfUrl } : undefined,
+ ).rss2();
}
export function generateAtomFeed(
@@ -89,6 +101,13 @@ export function generateAtomFeed(
emails: EmailData[],
baseUrl: string,
feedId: string,
+ selfUrl?: string,
): string {
- return buildFeed(feedConfig, emails, baseUrl, feedId).atom1();
+ return buildFeed(
+ feedConfig,
+ emails,
+ baseUrl,
+ feedId,
+ selfUrl ? { atom: selfUrl } : undefined,
+ ).atom1();
}