Skip to content

Repository files navigation

WebSub feed link debug tool

A small Hono app for testing whether WebSub subscribers find a hub when the discovery links are named and placed unconventionally — inline namespace declarations, at the bottom of the <channel> after all the items, per this gist — alongside a control feed with textbook markup, so a silent reader can be pinned on the format or on the reader.

Hub: https://rpc.rsscloud.io/websub (docs)

The three feeds

All three feeds are identical RSS 2.0 — same items, no rssCloud elements — except for how they declare, name, and place the WebSub discovery links.

/feed-classic.xml — the control. Textbook markup: xmlns:atom on the <rss> root, links at the top of the channel before the items.

<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    …
    <atom:link rel="hub"  href="https://rpc.rsscloud.io/websub"/>
    <atom:link rel="self" href="…/feed-classic.xml"/>
    <item>…</item>

If a reader misses this one, the markup isn't the problem — the reader or the platform is. The other two only mean something relative to it.

/feed-atom.xml — conventional element name, inline namespace, below the items:

<atom:link xmlns:atom="http://www.w3.org/2005/Atom" rel="hub"  href="https://rpc.rsscloud.io/websub"/>
<atom:link xmlns:atom="http://www.w3.org/2005/Atom" rel="self" href="…/feed-atom.xml"/>

/feed-websub.xml — same namespace URI bound to a websub prefix instead:

<websub:link xmlns:websub="http://www.w3.org/2005/Atom" rel="hub"  href="https://rpc.rsscloud.io/websub"/>
<websub:link xmlns:websub="http://www.w3.org/2005/Atom" rel="self" href="…/feed-websub.xml"/>

A namespace-aware parser should treat all three as the same element. A parser that string-matches on atom:link will miss /feed-websub.xml; one that stops scanning at the first <item> will miss both of the bottom-placed variants. That's the thing under test — with /feed-classic.xml there to tell you whether the reader was ever going to work at all.

One hostname per feed

Readers that resolve a bare hostname — Micro.blog does this — read the homepage's <head> and take the first <link rel="alternate"> they find. A single homepage listing all three feeds can therefore only ever hand such a reader the control, which makes the other two variants untestable through that path.

So each feed gets its own hostname under BASE_URL, and each of those homepages advertises that feed and nothing else:

Hostname Advertises
websub.example.com (apex) All three, side by side — the comparison view
classic.websub.example.com /feed-classic.xml only
atom.websub.example.com /feed-atom.xml only
websub.websub.example.com /feed-websub.xml only

The label is the feed id, so a fourth variant gets a hostname for free. Everything a feed emits is canonical to its own host — the autodiscovery link, the rel="self" in the feed, and the hub.url published for it. A subscriber that follows rel="self" as the canonical topic (as the spec says it should) lands where it started rather than being bounced to the apex, so a cross-host mismatch can't be confused for the discovery failure you're actually hunting.

Every feed is still served from every hostname; the hostname only decides what a page advertises. /post and /ping also still act on all three feeds regardless of which host you trigger them from.

This needs the three hostnames resolving to the server and a reverse proxy that passes the original Host through (X-Forwarded-Host is honoured and takes precedence). Three A records or one wildcard, whichever you prefer — listing the hosts explicitly keeps TLS simple, since a wildcard certificate has to be issued over a DNS-01 challenge while named hosts can use HTTP-01. With Caddy the whole proxy config is:

classic.websub.example.com, atom.websub.example.com, websub.websub.example.com, websub.example.com {
	reverse_proxy localhost:3000
}

Caddy preserves the incoming Host by default, so nothing further is needed. nginx does not — add proxy_set_header Host $host; there.

Locally nothing is needed at all: *.localhost resolves to loopback on macOS and most Linux setups, so http://atom.localhost:3000 just works.

Set FEED_SUBDOMAINS=0 to go back to serving everything from BASE_URL.

Running

npm install
npm start          # http://localhost:3000

The hub has to be able to fetch the feeds, so for a real end-to-end test expose the server publicly and set BASE_URL — each feed's hostname derives from it, and those are what end up in the rel="self" link and the hub.url you publish:

BASE_URL=https://your-tunnel.example npm start

The homepage warns when BASE_URL is still localhost.

For local development you can put settings in a .env file instead — copy .env-sample to .env and edit. npm start and npm run dev load it via node --env-file-if-exists, which needs Node 22.9+. Containers ignore .env; they take config from -e flags or compose environment:.

Routes

Route What it does
/ On a feed's hostname: that feed alone, advertised in the <head> as rel="alternate". On the apex: all three side by side. Both show the discovery markup each feed emits, recent feed fetches, and current config
/feed-classic.xml Control feed — root xmlns:atom, atom:link above the items
/feed-atom.xml Feed with inline-namespaced atom:link below the items
/feed-websub.xml Feed with inline-namespaced websub:link below the items
/post Form — adds an item to all feeds, then POSTs hub.mode=publish for each and shows the hub's response
/ping Notifies the hub for all feeds without changing them

Feed fetches are logged to stdout and listed on the homepage with their User-Agent, so you can watch the hub come back and re-read after a publish.

Docker

docker build -t websub-debug .
docker run -d -p 3000:3000 -e BASE_URL=https://websub.example.com websub-debug

Or run the published image without a checkout:

docker run -d -p 3000:3000 \
  -e BASE_URL=https://websub.example.com \
  ghcr.io/rsscloud/websub-debug:latest

Posted items are held in memory, so there's no volume — restarting resets both feeds to the single seeded item. examples/dockge/compose.yaml is a ready-to-paste Dockge stack.

To publish a multi-platform image to ghcr.io/rsscloud/websub-debug:

npm run docker:dry-run      # preview the tags
npm run docker:build-push   # build linux/amd64 + linux/arm64 and push

See scripts/README.md for tagging and auth details.

Environment

Variable Default Purpose
PORT 3000 Listen port
BASE_URL http://localhost:$PORT Public apex URL. Each feed's hostname is derived from it, and those are what appear in rel="self" and hub.url
HUB_URL https://rpc.rsscloud.io/websub Hub endpoint
SEND_LINK_HEADERS off Set to 1 to also advertise hub/self via HTTP Link headers
FEED_SUBDOMAINS on Set to 0 to serve every feed from BASE_URL instead of giving each its own hostname

SEND_LINK_HEADERS is off by default on purpose: Link headers are the discovery method the hub docs call primary, so leaving them on would let a subscriber find the hub without ever parsing the feed — which would defeat the experiment. Turn it on when you want to verify that path separately.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages