Atom feed for Astro

·
7 March 2025
·
22  mins read

When I moved this site to Astro, I’ve set up the feed using Astro’s RSS plugin, which generates an RSS 2.0 feed. But recently, I stumbled upon Nic Chan’s article where he described using the framework-agnostic Feed plugin to generate Atom 1.0 instead.

I’ve always preferred Atom over RSS so I made the switch immediately.

RSS vs Atom

RSS has wider compatibility and simpler structure compared to Atom. But it’s 2024, and worrying about Atom support feels a bit like fretting over whether your new car has a USB media player.

Atom has several compelling advantages that make it worth considering:

  • Better standardisation with clear, formal specifications
  • Superior support for rich content
  • Improved support for internationalisation
  • The smug satisfaction of using something slightly more sophisticated

And the best part? The Feed package supports both RSS and Atom, working seamlessly with Astro right out of the box—like finding out your new electric car still has a spot for your thumb drive music library after all.

Setting up Feed with Astro

Adding the jpmonette/feed package to your Astro project is straightforward.

1. Install jpmonette/feed

First, install the package using your package manager of choice:

Using yarn: yarn add feed Using pnpm: pnpm add feed

2. Create endpoint

Create an API endpoint in Astro to generate your feed by creating a file at src/pages/feed.xml.js:

// src/pages/feed.xml.js
import { Feed } from 'feed';
import { getCollection } from 'astro:content';

export async function GET(context) {
  const blog = await getCollection('blog');
  const siteUrl = 'https://yoursite.com';
  
  const feed = new Feed({
    title: 'Your Site Name',
    description: 'Your site description',
    id: siteUrl,
    link: siteUrl,
    language: 'en',
    favicon: `${siteUrl}/favicon.ico`,
    copyright: `All rights reserved ${new Date().getFullYear()}`,
    feedLinks: {
      rss: `${siteUrl}/rss.xml`,
    },
    author: {
      name: 'Your Name',
    },
  });
  
  blog.forEach((post) => {
    const url = `${siteUrl}/blog/${post.slug}`;
    
    feed.addItem({
      title: post.data.title,
      id: url,
      link: url,
      description: post.data.description,
      content: post.body, // Or process this with markdown if needed
      author: [
        {
          name: post.data.author || 'Your Name',
        },
      ],
      date: new Date(post.data.publishDate),
    });
  });
  
  return new Response(feed.rss2(), {
    headers: {
      'Content-Type': 'application/xml',
    },
  });
}

Declare the feed by adding a link to it in your site’s <head> section:

<link rel="alternate" type="application/atom+xml" title="Your Site Atom Feed" href="/feed.xml">

Parsing Markdown

After following the steps above, I found my feed serving raw Markdown instead of proper HTML.

The Astro RSS documentation recommends using SanitizeHTML and MarkdownIt to parse the content properly.

1. Install the packages

Install the necessary packages:

Using yarn: yarn add sanitize-html markdown-it Using pnpm: pnpm add sanitize-html markdown-it

2. Modify the endpoint

Modify src/pages/feed.xml.js to parse the content:

// src/pages/feed.xml.js
import { Feed } from 'feed';
import { getCollection } from 'astro:content';
import sanitizeHtml from 'sanitize-html';
import MarkdownIt from 'markdown-it';

export async function GET(context) {
  const siteUrl = 'https://yoursite.com';
  
  // Initialize markdown parser
  const md = new MarkdownIt({
    html: true,         // Enable HTML tags in source
    breaks: true,       // Convert '\n' in paragraphs into <br>
    linkify: true       // Autoconvert URL-like text to links
  });
  
  // Create feed instance
  const feed = new Feed({
    title: 'Your Site Name',
    description: 'Your site description',
    id: siteUrl,
    link: siteUrl,
    language: 'en',
    favicon: `${siteUrl}/favicon.ico`,
    copyright: `All rights reserved ${new Date().getFullYear()}`,
    feedLinks: {
      atom: `${siteUrl}/feed.xml`,
    },
    author: {
      name: 'Your Name',
    },
    updated: new Date(),
  });

  const blog = await getCollection('blog');

  // Add each post to the feed
  blog.forEach((post) => {
    const url = `${siteUrl}/blog/${post.slug}`;

    // Parse markdown to HTML
    const htmlContent = md.render(post.body);
    
    // Sanitize HTML to remove potentially unsafe tags
    const sanitizedContent = sanitizeHtml(htmlContent, {
      allowedTags: sanitizeHtml.defaults.allowedTags.concat(['img', 'h1', 'h2', 'h3']),
      allowedAttributes: {
        ...sanitizeHtml.defaults.allowedAttributes,
        img: ['src', 'alt', 'title'],
        a: ['href', 'name', 'target', 'rel']
      },
      // Transform relative URLs to absolute URLs
      transformTags: {
        'a': (tagName, attribs) => {
          if (attribs.href && attribs.href.startsWith('/')) {
            return {
              tagName: 'a',
              attribs: {
                ...attribs,
                href: `${siteUrl}${attribs.href}`,
                target: '_blank',
                rel: 'noopener'
              }
            };
          }
          return { tagName, attribs };
        },
        'img': (tagName, attribs) => {
          if (attribs.src && attribs.src.startsWith('/')) {
            return {
              tagName: 'img',
              attribs: {
                ...attribs,
                src: `${siteUrl}${attribs.src}`
              }
            };
          }
          return { tagName, attribs };
        }
      }
    });

    feed.addItem({
      title: post.data.title,
      id: url,
      link: url,
      description: post.data.description || '',
      content: sanitizedContent,
      author: [
        {
          name: post.data.author || 'Your Name',
        },
      ],
      date: new Date(post.data.publishDate),
      updated: post.data.modified ? new Date(post.data.modified) : new Date(post.data.publishDate)
    });
  });

  return new Response(feed.atom1(), {
    headers: { 'Content-Type': 'application/atom+xml' }
  });
}

Multiple feeds

Can’t decide between RSS and Atom? Why not both? Creating endpoints for each format allows readers to subscribe using their preferred method. Here’s how to set it up:

Create two endpoint files and modify headers returned.

For RSS 2.0 at /rss.xml:

// src/pages/rss.xml.js
return new Response(feed.rss2(), {
  headers: { 'Content-Type': 'application/xml' }
});

For Atom at /atom.xml:

src/pages/atom.xml.js
return new Response(feed.atom1(), {
  headers: { 'Content-Type': 'application/atom+xml' }
});

Wrapping up

Setting up Atom feeds with Astro and the Feed package greatly improves how my content is delivered to subscribers. The standardised format ensures better compatibility with modern feed readers while maintaining all the rich content from my posts.

Whether you choose RSS, Atom, or both, making your content accessible through feeds gives readers more control over how they consume your work—something that aligns perfectly with the principles of owning your data.

Don't miss a post

Join 1000+ others and get new posts delivered to your inbox.

I hate spam and won't send you any.