Codingfish Skip to content

Validating a feed, and what validators do not catch

Passing the W3C validator means the document is well-formed and conforms. It does not mean readers will show your posts, and the three failures that cost the most subscribers are all legal XML.

A feed that fails validation usually announces itself: readers refuse it, the subscribe button does nothing, and somebody complains within a day. That is the easy case. The expensive case is a feed that validates cleanly, is accepted everywhere, and quietly does not do what its author thinks it does.

The W3C Feed Validation Service checks two things. Is the document well-formed XML, and does it conform to the RSS 2.0 or Atom specification. Both are worth checking and neither is a statement about behaviour. A feed can satisfy both and still show a reader nothing.

What the validator will tell you, and what it costs to ignore

Three of its complaints are worth acting on immediately.

A missing self-referencing link. RSS 2.0 has no element for a feed's own address, so the convention borrowed one from Atom: <atom:link rel="self" href="…"/> inside <channel>, with the Atom namespace declared on the root element. The validator reports its absence as a recommendation rather than an error, which is why it is the single most commonly omitted element in the wild. It matters when a feed is discovered through an aggregator that needs to record where the feed lives independently of where it found it.

An unescaped ampersand. A bare & in a title is not valid XML, and this one does fail loudly. It is worth mentioning only because it is nearly always a templating mistake rather than an authoring one: the title was escaped once for HTML, then interpolated into XML by a different code path that assumed it was already safe.

A pubDate in the wrong format. RSS 2.0 specifies RFC 822 date-times — Tue, 08 Dec 2026 09:00:00 +0000. Atom uses RFC 3339, which is the ISO-8601 shape everybody reaches for by habit. Put an ISO date in an RSS pubDate and strict readers ignore it, which means the item sorts by whenever the reader first saw it. The validator catches this. Whatever generated the feed did not.

The three it will not tell you

Now the ones that pass.

A guid that changes when the post does. The guid element is the identity of an entry. Readers use it to decide whether they have seen something before. If the guid is derived from anything that can change — a title slug, a hash of the body, a database row that gets rewritten on edit — then correcting a typo republishes the post to every subscriber as new. There is no error anywhere. The feed is valid, the reader is behaving correctly, and the author looks like they cannot stop posting the same article.

The fix is to make the guid depend on the thing that does not change, usually the entry's creation, and to say so explicitly with isPermaLink="false" when the value is not a URL. A permanent URL is the simplest correct answer and the reason most feeds get this right by accident.

A lastBuildDate that is regenerated on every build. This one is subtle enough to be worth the paragraph. lastBuildDate says when the channel last changed. Static site generators tend to set it to the build timestamp, because that is trivially available. A site that rebuilds nightly for unrelated reasons then advertises a change every night. Conditional-request machinery downstream — If-Modified-Since, cache validators, a reader's own polling heuristic — is being told to look again, every time, forever. Nothing breaks. The feed simply costs more to serve than it should, and the reader learns that this feed's freshness signal means nothing.

Derive it from the newest item's date instead. If nothing was published, nothing changed.

Content that is valid XML and unreadable HTML. <description> may carry either a summary or the full entry, and if it carries markup that markup has to be escaped or wrapped in CDATA. Both are legal. What no validator checks is whether the result renders: a relative image path that resolved on the page will not resolve in a reader that fetched the feed from a different origin, and a stylesheet class that carried the layout is simply absent. The feed validates. The post arrives looking broken, in an application the author never opens.

The check that actually predicts behaviour

Validation answers a question about the document. What you want answered is a question about the readers, and there is only one honest way to get it: subscribe to your own feed in something you did not write, and then edit a post.

That second half is the part people skip. A fresh subscription proves the feed parses. Editing a published post is what exposes the guid, and it is the failure that costs subscribers rather than merely annoying them, because it looks like spam and readers unsubscribe from spam.

Two readers are enough, and they should disagree with each other — one that polls aggressively and one that caches politely. The disagreements are the interesting part.

If you want the underlying document to make sense first, the anatomy of a feed covers which elements a reader actually needs and why three of them carry almost every bug.

Where the errors come from

Almost none of this is written by hand any more, which changes what the likely causes are.

A feed produced by a static site generator inherits its bugs from a template that somebody wrote once and nobody has read since. The failure mode is uniformity: every entry is wrong the same way, which makes it easy to spot and easy to fix in one place. Check the generated output rather than the template, because the template is usually correct and the data being handed to it is not.

A feed produced by a CMS plugin inherits its bugs from whatever the CMS considers an entry. This is where the guid problems come from, because the CMS has an internal identifier that seems stable right up until content is moved between sections, restored from a revision, or imported from somewhere else. The forum and comment platforms catalogued on this site mostly emit feeds, and mostly derive the guid from a post's permalink — which is correct until somebody enables a different permalink scheme.

A feed assembled by hand, in a template written for this one site, is the rarest and the most likely to be right, because the person writing it had to read the specification to write it at all.

What is worth automating

Two checks, cheaply, in whatever runs the site's build.

Fetch the feed after the build and assert that the number of items is what you expect, and that the newest item's date matches the newest published entry. That catches an empty feed, a truncated feed and a build that ran before a scheduled post was due — three failures that produce a perfectly valid document.

Then assert that no guid appears twice. It is one line, it never fires, and on the day it does it will have saved a mailing to every subscriber.