DEV Community

Roger Rajaratnam
Roger Rajaratnam

Posted on • Originally published at sourcier.uk

Adding an RSS feed to an Astro blog

Original post: Adding an RSS feed to an Astro blog

Series: Part of How this blog was built — documenting every decision that shaped this site.

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/rss
Enter fullscreen mode Exit fullscreen mode

The 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();
}
Enter fullscreen mode Exit fullscreen mode

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(/* ... */);
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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}/`,
Enter fullscreen mode Exit fullscreen mode

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",
  // ...
});
Enter fullscreen mode Exit fullscreen mode

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(""),
Enter fullscreen mode Exit fullscreen mode

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.xml
Enter fullscreen mode Exit fullscreen mode

The 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"
/>
Enter fullscreen mode Exit fullscreen mode

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(""),
  });
}
Enter fullscreen mode Exit fullscreen mode

Top comments (0)