mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
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 </body> tag (regex now falls back to capturing everything after the opening <body>) - 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 <noreply@anthropic.com>
This commit is contained in:
@@ -93,7 +93,7 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
|
|||||||
|
|
||||||
const emailAddress = `${feedId}@${env.DOMAIN}`;
|
const emailAddress = `${feedId}@${env.DOMAIN}`;
|
||||||
const rssUrl = `https://${env.DOMAIN}/rss/${feedId}`;
|
const rssUrl = `https://${env.DOMAIN}/rss/${feedId}`;
|
||||||
|
const atomUrl = `https://${env.DOMAIN}/atom/${feedId}`;
|
||||||
|
|
||||||
return c.html(
|
return c.html(
|
||||||
<Layout title={`${feedConfig.title} - Emails`}>
|
<Layout title={`${feedConfig.title} - Emails`}>
|
||||||
@@ -114,6 +114,7 @@ emailsRouter.get("/feeds/:feedId/emails", async (c) => {
|
|||||||
<div>
|
<div>
|
||||||
<CopyField label="Email Address:" value={emailAddress} />
|
<CopyField label="Email Address:" value={emailAddress} />
|
||||||
<CopyField label="RSS Feed:" value={rssUrl} />
|
<CopyField label="RSS Feed:" value={rssUrl} />
|
||||||
|
<CopyField label="Atom Feed:" value={atomUrl} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = `https://${c.env.DOMAIN}`;
|
const baseUrl = `https://${c.env.DOMAIN}`;
|
||||||
|
const selfUrl = new URL(c.req.url).origin + `/atom/${feedId}`;
|
||||||
const atomXml = generateAtomFeed(
|
const atomXml = generateAtomFeed(
|
||||||
feedData.feedConfig,
|
feedData.feedConfig,
|
||||||
feedData.emails,
|
feedData.emails,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
feedId,
|
feedId,
|
||||||
|
selfUrl,
|
||||||
);
|
);
|
||||||
const linkHeader = [
|
const linkHeader = [
|
||||||
`<${baseUrl}/hub>; rel="hub"`,
|
`<${baseUrl}/hub>; rel="hub"`,
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ export async function handle(c: Context<{ Bindings: Env }>): Promise<Response> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const baseUrl = `https://${c.env.DOMAIN}`;
|
const baseUrl = `https://${c.env.DOMAIN}`;
|
||||||
|
const selfUrl = new URL(c.req.url).origin + `/rss/${feedId}`;
|
||||||
const rssXml = generateRssFeed(
|
const rssXml = generateRssFeed(
|
||||||
feedData.feedConfig,
|
feedData.feedConfig,
|
||||||
feedData.emails,
|
feedData.emails,
|
||||||
baseUrl,
|
baseUrl,
|
||||||
feedId,
|
feedId,
|
||||||
|
selfUrl,
|
||||||
);
|
);
|
||||||
const linkHeader = [
|
const linkHeader = [
|
||||||
`<${baseUrl}/hub>; rel="hub"`,
|
`<${baseUrl}/hub>; rel="hub"`,
|
||||||
|
|||||||
@@ -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
|
// Email content is stored as a full HTML document. Feed readers expect only
|
||||||
// the body fragment in <description>/<content:encoded>, not a full document.
|
// the body fragment in <description>/<content:encoded>, not a full document.
|
||||||
export function extractBodyContent(html: string): string {
|
export function extractBodyContent(html: string): string {
|
||||||
const match = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
const withClose = html.match(/<body[^>]*>([\s\S]*?)<\/body>/i);
|
||||||
return match ? match[1] : html;
|
if (withClose) return withClose[1];
|
||||||
|
// Some HTML emails omit </body>; capture everything after the opening tag
|
||||||
|
const withoutClose = html.match(/<body[^>]*>([\s\S]*)/i);
|
||||||
|
if (withoutClose) return withoutClose[1].replace(/<\/html>\s*$/i, "");
|
||||||
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFeed(
|
function buildFeed(
|
||||||
@@ -25,6 +29,7 @@ function buildFeed(
|
|||||||
emails: EmailData[],
|
emails: EmailData[],
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
feedId: string,
|
feedId: string,
|
||||||
|
selfUrl?: { rss?: string; atom?: string },
|
||||||
): Feed {
|
): Feed {
|
||||||
const feed = new Feed({
|
const feed = new Feed({
|
||||||
title: feedConfig.title,
|
title: feedConfig.title,
|
||||||
@@ -39,8 +44,8 @@ function buildFeed(
|
|||||||
generator: "kill-the-news",
|
generator: "kill-the-news",
|
||||||
copyright: `Copyright © ${new Date().getFullYear()} ${feedConfig.title}`,
|
copyright: `Copyright © ${new Date().getFullYear()} ${feedConfig.title}`,
|
||||||
feedLinks: {
|
feedLinks: {
|
||||||
rss: `${baseUrl}/rss/${feedId}`,
|
rss: selfUrl?.rss ?? `${baseUrl}/rss/${feedId}`,
|
||||||
atom: `${baseUrl}/atom/${feedId}`,
|
atom: selfUrl?.atom ?? `${baseUrl}/atom/${feedId}`,
|
||||||
},
|
},
|
||||||
author: feedConfig.author
|
author: feedConfig.author
|
||||||
? {
|
? {
|
||||||
@@ -80,8 +85,15 @@ export function generateRssFeed(
|
|||||||
emails: EmailData[],
|
emails: EmailData[],
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
feedId: string,
|
feedId: string,
|
||||||
|
selfUrl?: string,
|
||||||
): string {
|
): string {
|
||||||
return buildFeed(feedConfig, emails, baseUrl, feedId).rss2();
|
return buildFeed(
|
||||||
|
feedConfig,
|
||||||
|
emails,
|
||||||
|
baseUrl,
|
||||||
|
feedId,
|
||||||
|
selfUrl ? { rss: selfUrl } : undefined,
|
||||||
|
).rss2();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateAtomFeed(
|
export function generateAtomFeed(
|
||||||
@@ -89,6 +101,13 @@ export function generateAtomFeed(
|
|||||||
emails: EmailData[],
|
emails: EmailData[],
|
||||||
baseUrl: string,
|
baseUrl: string,
|
||||||
feedId: string,
|
feedId: string,
|
||||||
|
selfUrl?: string,
|
||||||
): string {
|
): string {
|
||||||
return buildFeed(feedConfig, emails, baseUrl, feedId).atom1();
|
return buildFeed(
|
||||||
|
feedConfig,
|
||||||
|
emails,
|
||||||
|
baseUrl,
|
||||||
|
feedId,
|
||||||
|
selfUrl ? { atom: selfUrl } : undefined,
|
||||||
|
).atom1();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user