fix(feed): advertise WebSub hub in RSS/Atom body

Readers like FreshRSS discover the hub from <atom:link rel="hub"> in the
feed XML, not the HTTP Link header. Without it they never subscribe and
only refresh on cache expiry (~30 min) instead of receiving an instant
push when a new email arrives.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-25 23:04:33 +02:00
parent 7297e06b94
commit e258206384
3 changed files with 31 additions and 0 deletions
+5
View File
@@ -14,6 +14,11 @@ verbatim as the GitHub Release notes — so what you write here is what ships.
### Fixed
- RSS and Atom feeds now advertise the WebSub hub inside the feed body
(`<atom:link rel="hub">`), not just in the HTTP `Link` header. Readers like
FreshRSS discover the hub from the XML, so they can now subscribe and receive
an instant push when a new email arrives instead of waiting up to the cache
`max-age` (30 min) to refresh.
- Subscription-confirmation detection now recognises a confirm email whose CTA
button carries the subscribe/subscription hint only in its visible text (e.g.
"Yes, subscribe me to this mailing list.") over an opaque tracking-redirect
+22
View File
@@ -130,6 +130,17 @@ describe("generateRssFeed", () => {
expect(result).toContain(`${BASE_URL}/rss/${FEED_ID}`);
});
it("advertises the WebSub hub in the RSS body", () => {
const result = generateRssFeed(
mockFeedConfig,
mockEmails,
BASE_URL,
FEED_ID,
);
expect(result).toContain('rel="hub"');
expect(result).toContain(`${BASE_URL}/hub`);
});
it("includes email entries as <item> elements", () => {
const result = generateRssFeed(
mockFeedConfig,
@@ -280,6 +291,17 @@ describe("generateAtomFeed", () => {
expect(result).toContain(`${BASE_URL}/atom/${FEED_ID}`);
});
it("advertises the WebSub hub in the Atom body", () => {
const result = generateAtomFeed(
mockFeedConfig,
mockEmails,
BASE_URL,
FEED_ID,
);
expect(result).toContain('rel="hub"');
expect(result).toContain(`${BASE_URL}/hub`);
});
it("includes rss alternate link", () => {
const result = generateAtomFeed(
mockFeedConfig,
+4
View File
@@ -35,6 +35,10 @@ function buildFeed(
// Public "website" for this feed: its own read URL (never the inbound address
// or an auth-gated admin path, so the feed output leaks neither).
link: `${baseUrl}/rss/${feedId}`,
// WebSub hub advertised in the feed body (<atom:link rel="hub">). Readers like
// FreshRSS discover the hub here, not from the HTTP Link header, so without it
// they never subscribe and only refresh on cache expiry.
hub: `${baseUrl}/hub`,
language: feedConfig.language,
updated: new Date(),
generator: "kill-the-news",