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

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages