Adding an RSS feed to an Astro blog
Content collections, draft filtering, and what an RSS reader actually expects
- Published
- 28 April 2026
- Read time
- 4 min read
Was this useful?
RSS is the oldest and most reliable way to follow a blog. No algorithm, no platform dependency, no notification settings. A reader checks the feed URL, sees new items, shows them. That simplicity is exactly why it’s worth supporting.
Adding an RSS feed to an Astro site is straightforward with the @astrojs/rss
package. There are a few things to get right: draft filtering, absolute URLs,
and a self-referencing link that validators expect.
Installing the package
Astro doesn’t ship with RSS support out of the box, but the official integration adds everything needed:
pnpm add @astrojs/rssThe feed endpoint
The feed lives at src/pages/rss.xml.js. Astro treats any .js file in
src/pages/ as a route, and a named GET export marks it as an endpoint that
generates output at build time.
A few parts of this are easy to get wrong.
Draft filtering
The draft flag alone isn’t enough. A post with draft: false and a future
pubDate is scheduled, not live. Filtering on !post.data.draft would leak
it into the feed before it’s published.
This blog distinguishes three publication states:
| State | Condition |
|---|---|
draft | draft: true |
scheduled | draft: false, future pubDate |
published | draft: false, past or current pubDate |
The isPubliclyPublished utility returns true only for the published state:
export function isPubliclyPublished(post: { data: { draft: boolean; pubDate: Date };}): boolean { return !post.data.draft && post.data.pubDate <= new Date();}If you haven’t extracted this into a utility, the inline equivalent is:
const now = new Date();const posts = (await getCollection("posts")) .filter((post) => !post.data.draft && post.data.pubDate <= now) .sort(/* ... */);Do not use import.meta.env.DEV to conditionally include drafts. The RSS feed
should never expose unpublished content, regardless of the build environment.
Sorting
Posts are sorted by pubDate descending so the most recent item appears first.
Most RSS readers display items in the order they appear in the feed XML, so the
sort order matters.
.sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf())Post links
The link value uses post.id, which in Astro’s Content Layer API is the
folder name of each post. For a post at collections/posts/rss-feed-astro/index.md,
post.id is rss-feed-astro. Prefixing it with /blog/ produces the correct
page URL, with no slug manipulation needed.
link: `/blog/${post.id}/`,Absolute URLs
The site property on the rss() call is context.site, the value set in
astro.config.mjs:
export default defineConfig({ site: "https://sourcier.uk", // ...});The @astrojs/rss package uses this to resolve relative link values into
absolute URLs. Without site configured, feed items would have relative URLs
that most RSS readers can’t navigate.
atom:link self-reference
RSS validators and some readers expect an <atom:link rel="self"> element in
the channel, pointing back to the feed’s own URL. The atom namespace must also
be declared on the root <rss> element, which the @astrojs/rss package handles
via the xmlns option:
xmlns: { atom: "http://www.w3.org/2005/Atom",},customData: [ `<language>en-gb</language>`, `<atom:link href="${context.site}rss.xml" rel="self" type="application/rss+xml"/>`,].join(""),Without this, the W3C validator flags a “missing atom:link” warning and some readers cannot determine the canonical feed URL.
Validating the feed
Before deploying, it’s worth running the built feed through the
W3C Feed Validation Service or
RSS Board’s validator. Common
mistakes like missing pubDate, non-absolute link values, and invalid XML
characters in post content all surface here before they cause problems in readers.
Build the site locally and check dist/rss.xml:
pnpm build && open dist/rss.xmlThe raw XML should be readable in the browser. If the browser shows a parse error, something in the feed is malformed.
Adding the feed autodiscovery link
RSS readers look for a <link rel="alternate"> tag in the page <head> to
discover the feed URL automatically. Add it to BaseLayout.astro:
<link rel="alternate" type="application/rss+xml" title="Sourcier RSS Feed" href="/rss.xml"/>With this in place, browsers and readers that support RSS autodiscovery will surface the feed when a user visits any page on the site.
The complete feed endpoint and autodiscovery link are in the sourcier.uk repository.
Full code listing
import rss from "@astrojs/rss";import { getCollection } from "astro:content";import { isPubliclyPublished } from "../utils/drafts";
export async function GET(context) { const posts = (await getCollection("posts")) .filter(isPubliclyPublished) .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());
return rss({ title: "Sourcier — Blog", description: "Practical software engineering writing for people transitioning into tech, engineers growing in confidence, and teams improving engineering practice.", site: context.site, xmlns: { atom: "http://www.w3.org/2005/Atom", }, items: posts.map((post) => ({ title: post.data.title, description: post.data.description, pubDate: post.data.pubDate, link: `/blog/${post.id}/`, })), customData: [ `<language>en-gb</language>`, `<atom:link href="${context.site}rss.xml" rel="self" type="application/rss+xml"/>`, ].join(""), });}Working on something similar?
Need help raising the bar?
I help teams improve engineering practice through hands-on delivery, pragmatic reviews, and mentoring. If you want a second pair of eyes or practical support, let's talk.
- Engineering practice review
- Hands-on delivery
- Team mentoring
If this has been useful, you can back the writing with a one-off tip through a secure Stripe checkout.
Free · Practical · One email per post
Get practical engineering notes
One short email when a new article goes live. Useful if you are breaking into tech, growing as an engineer, or improving engineering practice on your team.
Comments
Loading comments…
Leave a comment