Static Architecture Web Architecture Signal vs Noise

RSS Is Quietly Winning Again. Static Sites Should Ship a Feed.

Static Signal
A vast brass pneumatic dispatch system in a dark industrial chamber, glass capsules carrying glowing amber content cartridges through copper tubing toward distant subscriber stations, neon orange and teal light pulsing along the pipework, dark steampunk signal distribution network

Every couple of years someone declares RSS dead. The latest obituary was somewhere around 2013, when Google Reader shut down and the obvious conclusion was that nobody wanted to subscribe to feeds anymore. The conclusion was wrong. RSS didn’t die — it went underground, became plumbing, and is now quietly powering more of the modern web than the platforms trying to replace it.

Substack imports newsletters via RSS. Bluesky’s custom feed generators consume RSS as input. Mastodon exposes every account as an Atom feed and lets users follow any RSS source through a relay. Every “AI assistant that summarizes your daily reads” is, under the hood, polling a list of feed URLs. Reeder, Inoreader, Feedly, NetNewsWire, and Readwise Reader are all having their best years in a decade — Reeder’s iOS install base in 2026 is higher than it was at the Google Reader peak. The feed reader didn’t disappear. It just stopped being the only way readers consumed RSS.

If you publish a content site, you should ship a feed. The cost is fifty lines of build-time code and a single XML file in your output directory. The benefit is that every aggregator, every reader app, every AI agent, and every federated platform can pull your content directly — without an algorithm in the middle, without a platform deciding when to show it, and without you paying anything for the distribution.

Static sites are the perfect home for this. Same build-time pattern as OpenGraph images, same precompute-everything logic, same “the file lives on a CDN forever” model. Once it’s wired up, you’ll never think about it again, and your content will be readable in places you didn’t even know existed.


Where RSS Readers Actually Are in 2026

The “RSS is dead” claim leaned on the assumption that RSS lived in standalone reader apps and that those apps were dying. Both halves of that assumption have aged badly.

Standalone readers are doing fine. Reeder’s macOS and iOS apps were rebuilt from scratch in 2024 and the redesign drove a quiet revival — the dedicated-reader category exists again, and there are more polished options now than there were in 2012. Inoreader and Feedly run profitable freemium businesses. NetNewsWire is one of the most actively maintained open-source apps on the App Store. Readwise Reader has stitched RSS together with web articles, newsletters, podcasts, and YouTube into a single inbox and grown to half a million subscribers in three years.

But the real story is everywhere RSS shows up that isn’t a reader app. Substack pulls in RSS feeds and rebroadcasts them as newsletters. Bluesky’s feed-generator architecture lets developers build custom timeline algorithms whose inputs are arbitrary feed URLs. Mastodon servers expose every account at /@user.atom and consume RSS through bridges like Bridgy Fed and RSS-Parrot. Discord and Slack have first-class RSS bots. IFTTT and Zapier both treat RSS as one of their most-used trigger types. n8n has a built-in RSS node. Every Mastodon instance can subscribe to your feed without you doing anything.

And then there are the AI agents. The current generation of “personal research assistants” — Claude desktop with feed integrations, ChatGPT’s web browsing with feed parsing, Perplexity’s source ingestion, the dozens of indie agent frameworks — all of them consume RSS as a primary signal. When an agent is asked “what’s new on this site,” the first thing it looks for is a feed. If you don’t have one, the agent falls back to scraping HTML and probably gets it wrong. If you do, it reads structured data and gets it right.

The picture in 2026 is that RSS is the lingua franca of content distribution. Almost nothing consumes RSS as RSS anymore — the readers are mostly invisible — but the format is what every other system uses to talk to your site.


Why Platforms Hate RSS — and Why That’s the Point

RSS has one feature that makes platforms allergic to it: the reader chooses what they see and when. There is no impressions-based monetization. There is no algorithm to inject. There are no notifications the platform controls. The reader pulls; the publisher just publishes.

This is exactly why RSS keeps coming up in conversations about platform exhaustion. Every few months a new wave of writers leaves Twitter, then Substack, then Medium, then Beehiiv, looking for a place where their work isn’t gated by an opaque ranking system. The answer that keeps emerging is “own your domain, publish there, syndicate via RSS.” Not because RSS is hip — because RSS is the only format that lets readers subscribe without an intermediary holding the relationship.

For publishers, this is a moat. A platform can ban your account, demote your post, change its algorithm, get acquired, or shut down. None of that touches your RSS feed. The readers who subscribed via Reeder, the newsletter that pulls your feed nightly, the Mastodon instance that follows your Atom URL, the AI agent that polls you weekly — all of those relationships continue regardless of what any platform decides. You shipped one XML file and you bought yourself a distribution channel that nobody can take away.

Platforms don’t want this for you. They want a publishing experience where every read happens on their surface, with their ads, behind their algorithm. That’s why every “modern” publishing tool tries to lock you into their reader, their newsletter, their app. RSS routes around that. Which is why static sites — the model that already routes around platforms — should ship a feed by default.


Atom or RSS 2.0? Pick Atom.

There are two feed formats in the wild and a lot of confusion about which to use. The short answer: pick Atom. It’s a better-specified, better-behaved format, and every reader and aggregator built in the last fifteen years parses it perfectly.

RSS 2.0 was finalized in 2003 and has known problems: dates aren’t required, IDs aren’t required (<guid> is optional), HTML in content isn’t well-defined, and the spec is famously vague about edge cases. It’s still everywhere because it’s old and everything supports it. But if you’re starting fresh in 2026, you don’t need to inherit any of those quirks.

Atom (RFC 4287) is what you want. It mandates <id> for every entry, uses unambiguous ISO 8601 timestamps, defines <content type="html"> precisely, supports namespaces cleanly, and is what every modern reader actually prefers when given the choice. Mastodon emits Atom. Most podcasts use RSS 2.0 only because Apple’s podcast spec demands it; that’s a niche, not a default.

Ship one Atom feed at /feed.xml. If you want to be polite to legacy crawlers, alias /rss.xml to the same file or generate a second RSS 2.0 version — but it’s almost never necessary. In a decade of debugging feed issues, I’ve yet to see a real reader that handles RSS 2.0 but not Atom. The reverse is common.


What’s Actually In an Atom Feed

The minimum viable Atom feed is shorter than most people expect. Here’s the entire structure:

<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>Static Signal</title>
  <link href="https://staticsignal.dev"/>
  <link rel="self" href="https://staticsignal.dev/feed.xml"/>
  <id>https://staticsignal.dev/</id>
  <updated>2026-04-26T17:10:00-07:00</updated>
  <author>
    <name>Kevin</name>
  </author>

  <entry>
    <title>RSS Is Quietly Winning Again</title>
    <link href="https://staticsignal.dev/posts/rss-is-quietly-winning-again-static-sites-should-ship-a-feed"/>
    <id>tag:staticsignal.dev,2026-04-26:/posts/rss-is-quietly-winning-again-static-sites-should-ship-a-feed</id>
    <published>2026-04-26T17:10:00-07:00</published>
    <updated>2026-04-26T17:10:00-07:00</updated>
    <summary>RSS never died — it became the underlayer for everything.</summary>
    <content type="html"><![CDATA[<p>Full HTML content of the post goes here…</p>]]></content>
  </entry>

  <!-- more entries -->
</feed>

A few details worth getting right:

The feed-level <id> and the per-entry <id> should be stable URIs — they’re how readers deduplicate entries across visits. The tag: URI scheme (RFC 4151) is the convention. It looks weird if you’ve never seen it, but it solves a real problem: if you ever change the URL of a post, the <id> stays the same, so readers don’t re-notify users about a “new” post that’s just the same one with a new permalink.

The rel="self" link is mandatory in well-formed Atom. It points the feed at its own canonical URL, which lets readers detect when a feed has been republished from a copy.

<updated> is on both the feed and each entry. Feed-level <updated> is the most recent change to anything in the feed; entry-level is when that specific entry was last edited. Don’t confuse it with <published> — published is when the entry first appeared, updated is when it last changed.

<content type="html"> should contain the full post, wrapped in CDATA to avoid escaping every < in your post body. Yes, the full post. We’ll come back to that.


Build-Time Generation in About Fifty Lines

The build-time pattern is identical to the one already used for OpenGraph images. Iterate the content, render a template, write a file. Here’s what it looks like for a Next.js static site with Markdown posts:

// scripts/generate-feed.ts
import { writeFileSync } from 'node:fs'
import { getAllPosts } from '../src/lib/content'

const SITE_URL = 'https://staticsignal.dev'
const SITE_TITLE = 'Static Signal'
const SITE_AUTHOR = 'Kevin'

function escape(s: string) {
  return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
}

function entry(post: Post) {
  const url = `${SITE_URL}/posts/${post.slug}`
  const date = new Date(post.publishedAt).toISOString()
  const updated = new Date(post.updatedAt ?? post.publishedAt).toISOString()
  return `  <entry>
    <title>${escape(post.title)}</title>
    <link href="${url}"/>
    <id>tag:staticsignal.dev,${date.slice(0, 10)}:/posts/${post.slug}</id>
    <published>${date}</published>
    <updated>${updated}</updated>
    <summary>${escape(post.excerpt)}</summary>
    <content type="html"><![CDATA[${post.html}]]></content>
  </entry>`
}

async function main() {
  const posts = (await getAllPosts())
    .filter(p => !p.draft)
    .sort((a, b) => +new Date(b.publishedAt) - +new Date(a.publishedAt))
    .slice(0, 50)

  const updated = new Date(posts[0].publishedAt).toISOString()

  const xml = `<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>${SITE_TITLE}</title>
  <link href="${SITE_URL}"/>
  <link rel="self" href="${SITE_URL}/feed.xml"/>
  <id>${SITE_URL}/</id>
  <updated>${updated}</updated>
  <author><name>${SITE_AUTHOR}</name></author>
${posts.map(entry).join('\n')}
</feed>`

  writeFileSync('public/feed.xml', xml)
  console.log(`Wrote ${posts.length} entries to public/feed.xml`)
}

main()

Wire it into the build script:

{
  "scripts": {
    "build": "tsx scripts/generate-feed.ts && next build"
  }
}

Add the auto-discovery link to your root layout so reader apps can find the feed when a user pastes your homepage URL:

// src/app/layout.tsx
export const metadata = {
  alternates: {
    types: {
      'application/atom+xml': '/feed.xml',
    },
  },
}

That’s the whole feature. The build runs the script, the script writes public/feed.xml, the static export deploys it as a regular asset, and your CDN serves it forever at https://yoursite.com/feed.xml with whatever cache headers you set on the rest of your static output.


Full Content vs Summary: Ship the Whole Post

There’s an old debate in feed publishing: should the feed contain the full text of each post, or just a summary that links back to the site? The answer in 2026 is full content, every time, with no apology.

The summary-only school of thought comes from the page-view era. The reasoning was that if your full content is in the feed, readers consume it inside their reader app, never visit the site, and never see the ads or the newsletter signup. Page views were the metric, summaries were the lure that pulled readers back.

That model is dead. If you’re a content site running on a static host with no ads and no analytics dashboard tied to revenue, you have nothing to lose by shipping the full text. The reader gets a better experience. The feed becomes the canonical reading surface for people who prefer it. AI agents that summarize your work get accurate text to work from instead of scraping HTML.

The bigger reason: in a world where readers are spread across Reeder, Inoreader, Mastodon, Bluesky, Substack imports, and AI inboxes, the feed is often the only place your content gets read. If you ship summaries, you’re betting that readers will click through. Most won’t. They’ll skim the title, see “read more,” and move on. If you ship the full content, the reader gets your actual words wherever they are. That’s the whole point.

There’s a side effect worth noting: full-content feeds force you to write better HTML. Your post is going to be rendered in dozens of reader apps with completely different stylesheets — some dark mode, some serif body, some that strip everything but <p> and <a>. If your post depends on bespoke CSS to be readable, the feed version will look broken. Writing semantic, well-structured HTML in your post body fixes that, and it makes your site itself more accessible as a happy byproduct.


Auto-Discovery, Conventions, and the Two URLs Readers Try First

Reader apps don’t make users find the feed URL manually. When a user pastes https://yoursite.com/ into Reeder, the app fetches the homepage and looks for an autodiscovery link in the HTML head:

<link rel="alternate" type="application/atom+xml" title="Static Signal" href="/feed.xml" />

If that’s there, the user clicks “subscribe” and it works. If it’s missing, the reader falls back to trying common paths — /feed, /feed.xml, /rss, /rss.xml, /atom.xml, /index.xml — and gives up if none respond. Most do something between those two: ship the discovery link, and put the feed at one of the conventional paths so manual users can guess it.

The two paths readers try first are /feed.xml and /feed. Pick one as canonical and 301 the other if you can. On a pure-static host where you can’t do redirects, just generate both files with identical content. They’re tiny.

Set the content type correctly. Atom should be served as application/atom+xml; RSS as application/rss+xml. Most static hosts get this right by default for .xml extensions, but check your specific host — Cloudflare Pages, Netlify, and Vercel all serve .xml as application/xml by default, which works but isn’t quite right. A _headers file or equivalent fixes it:

/feed.xml
  Content-Type: application/atom+xml; charset=utf-8

One more convention: add <atom:link rel="self"> pointing at your feed’s own URL. It’s required by some validators and lets relay services detect when a feed is being republished from another source.


Tradeoffs, Honestly

Build-time feed generation isn’t free. Worth being clear about what you’re trading.

No real-time updates. A new post doesn’t appear in the feed until the next build. For a typical blog publishing a few times a week, this is irrelevant — your CI runs on every push, and the feed updates with the post. For high-frequency publishing (news sites, live event coverage), runtime generation makes sense. For a content site, build-time is fine.

No personalization. A static feed shows the same content to every subscriber. If you want gated content, paid tiers, or per-user feeds, you need authentication and runtime logic. Most publishers don’t, and the ones who do are usually better served by a different system entirely (a newsletter platform, a membership tool) than by trying to wedge personalization into RSS.

No analytics. You don’t know who subscribes or what they read. Some people view this as a feature; some view it as a problem. If you genuinely need feed analytics, services like FeedPress sit in front of your feed and count requests. Most of the data they expose is uninteresting noise. The interesting data — “are people reading my work” — is better measured by other signals anyway.

Podcasts are a different shape. Podcast feeds need RSS 2.0 with the <itunes:> namespace and <enclosure> tags pointing at audio files. If you’re shipping a podcast, you need a podcast-specific feed in addition to (or instead of) a regular Atom feed. The build-time pattern still applies — just a different template.

Full-content feeds expose your bare HTML. If your post relies on JavaScript-driven interactivity, fancy CSS effects, or component-rendered embeds, those won’t survive in a feed reader. Most reader apps strip styles and scripts entirely. This forces a discipline of writing post bodies that are readable as plain HTML — which is a healthy constraint, not a problem.

These are real tradeoffs and they’re nothing compared to what you get. The static feed is one file, costs zero, runs on any CDN, and gives every aggregator on the web a clean way to pull your work.


The Pattern Behind the Pattern

This post is partly about RSS, but it’s mostly about the same idea that comes up every time the static-first model shows its hand. Any output that is a deterministic function of your content belongs in your build, not in a runtime function.

Sitemaps, OpenGraph images, search indexes, related-post graphs, JSON-LD blocks, AMP pages (if anyone still wants those), even thumbnails and image galleries — they’re all the same shape: a known transformation applied to known inputs at known times. Every one of them gets faster, cheaper, and more reliable when you compute it once at build and ship the result as a file. Every one of them gets slower, costlier, and more fragile when you defer it to a serverless function that re-derives the same answer on every request.

Frameworks tend to nudge you toward runtime versions of these features because runtime is where the framework’s leverage is. The framework’s authors want to show off their server-side capabilities. Their docs lead with the runtime API. Their templates ship with app/feed/route.ts instead of scripts/generate-feed.ts. The static alternative is rarely the default and almost never the recommendation.

But for a content site, runtime feed generation is paying a tax for flexibility you don’t have. Your posts are files in a repo. Your URLs are bounded. Your feed contents are a pure function of your content directory. There’s nothing dynamic to compute. The serverless function is just doing the work of cat-ing strings together on every request, and you’re billing it to do that.

Ship the feed at build time. Same for sitemaps. Same for OG images. Same for search. The static-first model isn’t about avoiding servers — it’s about being honest about which parts of your site are actually dynamic and not paying for runtime computation on the parts that aren’t. Almost nothing on a content site needs to be dynamic. The feed is one of the easiest places to start, and once you’ve shipped it, you’ll have an answer for the next person who tells you RSS is dead.

It’s not. It’s quietly winning, in places nobody’s looking. Make sure your site is one of them.