RSS Is Still the Best API You Can Ship
By Trent Writing & Content Development 4 min read
A feed is the cheapest public API in existence. No authentication, no rate limits to design, no client libraries, no deprecation policy, no support channel. You emit an XML document and every consumer that has existed for twenty-five years knows how to read it.
And it is consistently shipped badly, in ways that are individually small and collectively fatal.
The four ways a feed is broken
Item links that 404. The most common one, and the most invisible. If your site is configured with slashless URLs and your feed generator appends a trailing slash to every item link — which several do, by default — every entry in your feed points at a URL your origin does not serve. The feed validates. The XML is well-formed. Every link is dead.
Nobody catches this because nobody reads their own feed. The people who find out are subscribers, who do not report it; they unsubscribe.
Unescaped content. An ampersand in a headline, an angle bracket in a code sample, a stray ]]> in a CDATA section. The result is a document that is not well-formed XML, which most readers respond to by showing nothing at all. One bad title takes down the whole feed, including every item that was fine.
Wrong or missing dates. pubDate is what every reader sorts on. Omit it and your items arrive in arbitrary order. Set it to the build time and every item in the feed appears to have been published in the last five minutes, every time you deploy — which trains a reader to ignore you.
Everything in one feed. This one is subtler and it is the interesting one.
One feed per thing, not one feed per site
The instinct is that a site has a feed. It does not; a site has feeds for the things it publishes, and merging distinct streams into one destroys properties each of them had.
Take two streams that a single site might legitimately carry: articles, and a forward-looking calendar of upcoming events.
Articles have real publication dates, in the past, and are sorted newest-first. That is exactly the semantics pubDate was designed for.
Upcoming events have future dates. Putting a future date in pubDate is a semantic error — the item was not published then — and readers handle it inconsistently, several by hiding the item until the date arrives. And the correct sort is the opposite one: soonest first, ascending, because the value of the list is what is coming next.
So one feed cannot be both. It can be sorted newest-first or soonest-first. Its pubDate can be honest or present. Merging them means picking one stream to serve badly.
The second reason to keep them apart is consent. Someone who subscribed to a calendar feed subscribed to a calendar. Injecting essays into it is a change to what they agreed to receive, made without asking, and the only signal they can send back is to leave.
Two feeds, each declared with <link rel="alternate"> so both are discoverable, costs one extra route.
What to actually put in an item
Keep it small and keep it honest.
- Title — the headline, escaped. Not the headline plus your site name.
- Link — the absolute canonical URL, in exactly the form your canonicals use. If it does not byte-for-byte match the
<link rel="canonical">on the page, one of them is wrong. - Description — the standfirst. Not the first 200 characters of the body, which cuts mid-sentence; not the full body, which makes your feed the destination and your site the redundant part.
- pubDate — the real publication date, as a date, once, never touched again on republish.
That is the whole item. Categories are worth adding if your tags mean something. Almost nothing else is.
Assert it, because you will not read it
The reason feeds rot is that a feed is the one artifact on a site that its owner never looks at. It is not on a page, it does not appear in a screenshot, and a broken one produces no error anywhere you would see.
So the feed belongs in the build gate alongside everything else:
for (const link of feedLinks) {
if (link.endsWith('/')) fail(`<link> ends in a slash and will 404: ${link}`)
if (!link.startsWith(ORIGIN)) fail(`<link> is not on ${ORIGIN}: ${link}`)
}
for (const e of xmlErrors(xml)) fail(`feed.xml: ${e}`)
Well-formedness, absolute links, no trailing slashes, one pubDate per item, item count matching the number of published records. Five assertions, and between them they catch every failure listed above on the day it is introduced rather than in a support email six months later.
Why bother, in an era of feeds nobody reads
Because “nobody reads feeds” was only ever true of humans, and it was never entirely true of them either.
A feed is the machine-readable index of your archive. It is what an aggregator polls, what a newsletter tool ingests, what a script watches, what an automated system uses to notice that something changed without crawling you. It is a stable, versionless, permission-free contract that costs you a route file and keeps working while every fashionable integration format is replaced twice.
That is a good trade for forty lines and five assertions.
Read next
The whole archive-
SEO
A Taxonomy Is a Commitment, Not a Filing Convenience
Categories, subcategories and tags look like three flavours of the same idea. They are three different promises about permanence, and confusing them is how a site ends up with two answers to what a page is about.
-
Development
Timezones Are Not a Formatting Problem
A calendar day and an instant in time are different types, and most date bugs are the moment one is silently converted into the other. The fix is not better formatting — it is refusing the conversion.
-
Development
Content in Git Is a Database Decision
Putting your content in a repository looks like a tooling preference. It is actually a choice about consistency, history and who is allowed to write — and it has a specific expiry date you should decide in advance.
Keep reading
More on Writing & Content
Every vertical on this site carries its own writing, its upcoming events and its answers on one page — and the whole archive is available as a feed with a real publication date on every item.