Generating a valid feed
Building feed XML safely from application data, validating it, and advertising it so readers can find it.
Never build XML by concatenation
Hand-written string templates are how feeds break. One headline containing an ampersand, an emoji, or a stray tag produces invalid XML. Use a serialiser that escapes text nodes for you.
from xml.etree import ElementTree as ET
from email.utils import format_datetime
from datetime import datetime, timezone
rss = ET.Element("rss", {"version": "2.0"})
ch = ET.SubElement(rss, "channel")
ET.SubElement(ch, "title").text = "Example Dev Blog"
ET.SubElement(ch, "link").text = "https://example.com/"
ET.SubElement(ch, "description").text = "Notes on building web systems."
for post in posts: # each post: dict with title/url/id/published/body
it = ET.SubElement(ch, "item")
ET.SubElement(it, "title").text = post["title"]
ET.SubElement(it, "link").text = post["url"]
ET.SubElement(it, "guid", {"isPermaLink": "false"}).text = post["id"]
ET.SubElement(it, "pubDate").text = format_datetime(
post["published"].astimezone(timezone.utc))
ET.SubElement(it, "description").text = post["body"] # escaping handled here
ET.ElementTree(rss).write("feed.xml", encoding="utf-8", xml_declaration=True)const esc = (s) => String(s)
.replace(/&/g, "&").replace(/</g, "<")
.replace(/>/g, ">").replace(/"/g, """);
const item = (p) => [
"<item>",
" <title>" + esc(p.title) + "</title>",
" <link>" + esc(p.url) + "</link>",
' <guid isPermaLink="false">' + esc(p.id) + "</guid>",
" <pubDate>" + p.published.toUTCString() + "</pubDate>",
" <description>" + esc(p.summary) + "</description>",
"</item>"
].join("\n");💡
Escape the text and let a serialiser decide the structure. If you must concatenate, escape in this order — ampersand first — otherwise the escapes you just inserted get escaped again.
Validate and serve correctly
# well-formedness, then structure against the RSS 2.0 schema
xmllint --noout feed.xml
curl -sSL https://www.rssboard.org/files/schema.rng -o rss.rng
xmllint --noout --relaxng rss.rng feed.xml
# check what the reader will actually see
curl -sSI https://example.com/feed.xml | grep -i content-type
curl -s https://example.com/feed.xml | xmllint --format - | head -30| Mistake | Symptom in readers |
|---|---|
| Unescaped ampersand or angle bracket | The whole feed fails to parse |
| Relative URLs in link or guid | Links open on the reader's domain or fail |
| guid changes on every publish | Old items reappear as new |
| pubDate in ISO 8601 | Dates show as missing or fall back to fetch time |
Wrong Content-Type | Browser renders raw XML instead of offering subscription |
| Feed not updated when content is | Readers poll forever and never see the change |
Missing atom:link rel="self" | Aggregators cannot detect the canonical feed URL |
HTTP/1.1 200 OK
Content-Type: application/rss+xml; charset=utf-8
Cache-Control: max-age=300
ETag: "feed-2026-09-18-01"
Last-Modified: Fri, 18 Sep 2026 09:00:00 GMTAdvertise the feed in the document head so browsers and readers discover it, and keep the URL stable forever — subscribers are lost when a feed moves without a redirect.
<link rel="alternate" type="application/rss+xml"
title="Example Dev Blog" href="https://example.com/feed.xml"/>Publishing reliably
- Generate the feed in the same build as the pages, so it can never be newer or older than the content.
- Sort items newest first, and cap the list to a fixed number so the file stays small.
- Emit
lastBuildDatefrom the newest item, not from the current time — otherwise the feed looks changed on every regeneration. - Keep a stable
guidderived from the item's canonical URL or a content id, never from a timestamp. - Serve the feed with a short
Cache-Controland anETag; readers poll often and a 304 costs almost nothing. - Set a realistic
ttlor document your update cadence rather than relying on readers guessing.
FAQ
My feed validates but no reader shows it. Why?
Check the
Content-Type header, the link element in the page head, and whether the URL redirects. A 302 through an HTML error page is the most common cause of a silently rejected feed.Can I serve different content by User-Agent?
Avoid it. Cloaking feeds breaks caches and confuses validators. Serve one honest document and let the client choose what to display.
Related
The RSS 2.0 feed format Atom and how readers consume feeds
Last refreshed 2026-09-18.