feat: WebSub Atom support, HTML processing via linkedom, W3C badges

WebSub / PubSubHubbub:
- Hub now accepts both /rss/:id and /atom/:id topic URLs
- WebSubSubscription stores format ("rss" | "atom")
- notifySubscribers sends RSS or Atom XML with correct Content-Type
- verifyAndStoreSubscription sends correct topic URL per format
- CI paths-ignore docs/** to skip deploy on docs-only changes

HTML processing (linkedom + escape-html):
- New html-processor.ts: body extraction, script/iframe/object removal,
  event handler + javascript: URL stripping, mso-* style cleanup,
  plain text → <pre> with HTML escaping via escape-html
- feed-generator.ts and entries.ts use processEmailContent

Admin UI:
- W3C validation badges (Atom + RSS) on feed detail page

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Julien Herr
2026-05-22 21:12:10 +02:00
parent 1789870f27
commit a29e9ab372
13 changed files with 719 additions and 69 deletions
+6 -4
View File
@@ -59,18 +59,19 @@ hubRouter.post("/", async (c) => {
return c.text("Bad Request: hub.callback must use HTTPS", 400);
}
// Validate that topic matches a known RSS feed on this hub
// Validate that topic matches a known RSS or Atom feed on this hub
const topicPattern = new RegExp(
`^https://${env.DOMAIN.replaceAll(".", "\\.")}/rss/([^/]+)$`,
`^https://${env.DOMAIN.replaceAll(".", "\\.")}/(rss|atom)/([^/]+)$`,
);
const match = topic.match(topicPattern);
if (!match) {
return c.text(
"Bad Request: hub.topic must be an RSS feed URL on this hub",
"Bad Request: hub.topic must be an RSS or Atom feed URL on this hub",
400,
);
}
const feedId = match[1];
const format = match[1] as "rss" | "atom";
const feedId = match[2];
// Verify the feed exists before accepting any subscription
const feedConfig = await env.EMAIL_STORAGE.get(
@@ -99,6 +100,7 @@ hubRouter.post("/", async (c) => {
callbackUrl as string,
secret as string | undefined,
leaseSeconds,
format,
env,
),
);