Skip to content
Astro as endpoints 4 min read

Generating an RSS Feed

An RSS feed lets readers subscribe to your blog or changelog in their feed reader and get notified the moment you publish. In Astro, a feed is just another endpoint: a .xml.js (or .xml.ts) file in src/pages/ that exports a GET function returning generated XML. The official @astrojs/rss package turns a list of items into a spec-compliant feed in a few lines, and because endpoints run at build time by default, your feed is a fully static file with zero client JavaScript. This page wires a feed up to a content collection so it stays in sync with every post you write.

Installing the package

Add the integration helper with Astro’s CLI. It is a small utility — it does not register a runtime integration, it just exports the rss() function you call from an endpoint.

npx astro add rss

If you prefer to install manually:

npm install @astrojs/rss

You also need a site value in your config so the feed can generate absolute URLs, which RSS requires.

// astro.config.mjs
import { defineConfig } from "astro/config";

export default defineConfig({
  site: "https://example.com",
});

Without a site set, @astrojs/rss throws at build time because every item link and the channel link must be absolute. This is the single most common cause of a broken feed.

Creating the feed endpoint

Create a file named src/pages/rss.xml.js. The filename becomes the route, so this feed is served at /rss.xml. The exported GET function receives the endpoint context, which carries context.site — the value you configured above.

// src/pages/rss.xml.ts
import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import type { APIContext } from "astro";

export async function GET(context: APIContext) {
  const posts = await getCollection("blog");

  return rss({
    title: "DevCraftly Blog",
    description: "Practical guides for modern web developers.",
    site: context.site!,
    items: posts.map((post) => ({
      title: post.data.title,
      description: post.data.description,
      pubDate: post.data.pubDate,
      link: `/blog/${post.id}/`,
    })),
  });
}

Run a build and the feed is emitted as a static file:

Output:

22:14:03 ▶ /rss.xml
22:14:03   └─ /rss.xml (+3ms)
22:14:03 ✓ Completed in 412ms.

Open http://localhost:4321/rss.xml after npm run dev and you will see the generated XML. Validate it once with the W3C Feed Validator to confirm readers will accept it.

Pulling items from a content collection

The example above maps directly over a blog collection. Each item link is a relative path; @astrojs/rss prefixes it with site to produce the absolute URL. Sort newest-first and you have a conventional feed:

const posts = (await getCollection("blog"))
  .filter((post) => !post.data.draft)
  .sort((a, b) => b.data.pubDate.valueOf() - a.data.pubDate.valueOf());

The items array accepts these common fields:

FieldTypeNotes
titlestringItem headline.
descriptionstringSummary shown in the reader. Plain text or HTML.
pubDateDatePublish date; readers sort and dedupe on this.
linkstringRelative or absolute URL to the post.
contentstringOptional full HTML body (see below).
categoriesstring[]Optional tags.
customDatastringRaw XML appended inside the <item>.

Including full post content

To publish full articles rather than just summaries, render the Markdown body to HTML and pass it as content. Sanitize it first, since feed readers display this HTML directly.

import rss from "@astrojs/rss";
import { getCollection } from "astro:content";
import sanitizeHtml from "sanitize-html";
import MarkdownIt from "markdown-it";
import type { APIContext } from "astro";

const parser = new MarkdownIt();

export async function GET(context: APIContext) {
  const posts = await getCollection("blog");

  return rss({
    title: "DevCraftly Blog",
    description: "Practical guides for modern web developers.",
    site: context.site!,
    items: posts.map((post) => ({
      title: post.data.title,
      pubDate: post.data.pubDate,
      link: `/blog/${post.id}/`,
      content: sanitizeHtml(parser.render(post.body ?? ""), {
        allowedTags: sanitizeHtml.defaults.allowedTags.concat(["img"]),
      }),
    })),
  });
}

Styling the feed and autodiscovery

A raw .xml URL looks intimidating in a browser. Add an xslt stylesheet so casual visitors see something readable, and add a <link> in your site head so browsers and readers can autodiscover the feed.

---
// src/layouts/Base.astro
---
<head>
  <link
    rel="alternate"
    type="application/rss+xml"
    title="DevCraftly Blog"
    href={new URL("rss.xml", Astro.site)}
  />
</head>
return rss({
  title: "DevCraftly Blog",
  description: "Practical guides for modern web developers.",
  site: context.site!,
  items: [...],
  stylesheet: "/rss/styles.xsl",
});

Best practices

  • Always set site in astro.config.mjs so links resolve to absolute URLs.
  • Sort items newest-first and filter out drafts so subscribers never see unpublished work.
  • Use a stable link per item; readers key off it to avoid showing the same post twice.
  • Sanitize any HTML you place in content — feed readers render it without your CSP.
  • Keep pubDate a real Date from your collection schema, not a hand-formatted string.
  • Add an autodiscovery <link rel="alternate"> to every page so feed readers find it automatically.
  • Validate the output once with the W3C Feed Validator before announcing the feed.
Last updated June 14, 2026
Was this helpful?