Migrating between RSS, Atom and JSON Feed
Changing formats is mostly a mapping exercise, but the identifier rules decide whether subscribers see a migration or a re-notification of everything.
The element mapping
| Concept | RSS 2.0 | Atom | JSON Feed |
|---|---|---|---|
| Feed title | channel/title | feed/title | title |
| Feed URL | channel/link | feed/link rel=alternate | home_page_url |
| Self URL | atom:link rel=self | feed/link rel=self | feed_url |
| Item identifier | guid | entry/id | items[].id |
| Item URL | link | entry/link rel=alternate | url |
| Published | pubDate (RFC 822) | published (RFC 3339) | date_published |
| Updated | atom:updated | updated (required) | date_modified |
| Summary | description | summary | summary |
| Full content | content:encoded | content type=html | content_html |
| Author | dc:creator or author | author/name (required on entry or feed) | authors[].name |
| Enclosure | enclosure url type length | link rel=enclosure | attachments[].url |
| Categories | category | category term | tags[] |
def to_atom(it: Item) -> str:
# Atom requires an id, a title and an updated timestamp on every entry.
# A missing updated value is not optional: fall back to published.
updated = it.updated or it.published
return "".join([
"<entry>",
"<id>" + esc(it.id) + "</id>",
"<title>" + esc(it.title) + "</title>",
"<updated>" + rfc3339(updated) + "</updated>",
"<published>" + rfc3339(it.published) + "</published>",
"<link rel=\"alternate\" href=\"" + esc(it.url) + "\"/>",
"<author><name>" + esc(it.author or "Example Team") + "</name></author>",
"<summary type=\"text\">" + esc(it.summary) + "</summary>",
"<content type=\"html\">" + esc(it.html) + "</content>",
"<category term=\"" + esc(it.tags[0]) + "\"/>" if it.tags else "",
"</entry>",
])Keeping identifiers and consumers intact
- Choose one existing identifier field as the source of truth and reuse its exact values in the new format. Published items keep their existing identifiers forever.
- Publish the new format at a new URL. Never repurpose an existing feed URL for a different format, because readers cache the content type as well as the address.
- Serve both formats from the same application for a defined period, with an announcement on the site and in both feeds.
- Keep the old URL alive and redirect it only at the HTTP level with a 301 to the new format's URL, if you must consolidate. A 302 is safer for the first weeks because it is reversible.
- Update the discovery links on every page, but leave the old link present for a while so cached pages still work.
- Notify the directories and aggregators that matter, and check that the item identifiers survived on their side before removing the old feed.
- Remove the old format only after monitoring shows the old URL has been quiet for a full polling cycle of your slowest consumer.
- Every item has the same identifier in the new feed as in the old one.
- Every URL in the new feed is absolute and resolves with the same status as before.
- Dates round-trip through the new format without gaining or losing an hour.
- The content type of the old URL is unchanged for as long as the old URL is served.
- Enclosure URLs and lengths are byte-identical for podcast items.
⚠️
The one change that guarantees a support burden is changing item identifiers. Everything else in a format migration can be fixed with a second deploy; re-notified items cannot be un-notified, and users who see a thousand old posts marked unread will unsubscribe.
FAQ
Can one URL serve both RSS and Atom?
Only with content negotiation on Accept, which many readers do not send correctly and validators report confusingly. Two URLs, one link rel=alternate each, is the reliable approach.
How long should I keep the old feed?
At least a full polling cycle of your slowest consumer, which for a podcast directory can be weeks. Since a static file costs almost nothing, keep it indefinitely unless there is a specific reason to retire it.
Related
Choosing a feed format: RSS 2.0, Atom and JSON Feed Atom and how readers consume feeds
Last refreshed 2026-09-18.