mirror of
https://github.com/juherr/kill-the-news.git
synced 2026-06-20 22:03:48 +00:00
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:
@@ -14,6 +14,11 @@ verbatim as the GitHub Release notes — so what you write here is what ships.
|
|||||||
|
|
||||||
### Fixed
|
### 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
|
- Subscription-confirmation detection now recognises a confirm email whose CTA
|
||||||
button carries the subscribe/subscription hint only in its visible text (e.g.
|
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
|
"Yes, subscribe me to this mailing list.") over an opaque tracking-redirect
|
||||||
|
|||||||
@@ -130,6 +130,17 @@ describe("generateRssFeed", () => {
|
|||||||
expect(result).toContain(`${BASE_URL}/rss/${FEED_ID}`);
|
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", () => {
|
it("includes email entries as <item> elements", () => {
|
||||||
const result = generateRssFeed(
|
const result = generateRssFeed(
|
||||||
mockFeedConfig,
|
mockFeedConfig,
|
||||||
@@ -280,6 +291,17 @@ describe("generateAtomFeed", () => {
|
|||||||
expect(result).toContain(`${BASE_URL}/atom/${FEED_ID}`);
|
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", () => {
|
it("includes rss alternate link", () => {
|
||||||
const result = generateAtomFeed(
|
const result = generateAtomFeed(
|
||||||
mockFeedConfig,
|
mockFeedConfig,
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ function buildFeed(
|
|||||||
// Public "website" for this feed: its own read URL (never the inbound address
|
// 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).
|
// or an auth-gated admin path, so the feed output leaks neither).
|
||||||
link: `${baseUrl}/rss/${feedId}`,
|
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,
|
language: feedConfig.language,
|
||||||
updated: new Date(),
|
updated: new Date(),
|
||||||
generator: "kill-the-news",
|
generator: "kill-the-news",
|
||||||
|
|||||||
Reference in New Issue
Block a user