Why I added RSS feeds to my blog (and how I did it in Nuxt 3)

August 31, 2025

How I built RSS and Atom feed support for my Nuxt 3 website to let readers follow my content without social media algorithms.

Nuxtweb developmenttutorial
RSS and Atom feeds in Nuxt 3
Table of Contents

What are RSS and Atom feeds?

RSS (Really Simple Syndication) and Atom are XML formats for sharing new posts with readers. Instead of visiting each site by hand, readers subscribe to a feed. When a site publishes new content, the feed updates and readers see it in their feed reader.

Some popular web-based feed readers are FreshRSS, Feedly, Inoreader, and NewsBlur. Desktop apps like Fluent Reader and mobile apps like Reeder and Readrops are also available. I run a FreshRSS instance in my homelab and read updates through its web UI or the Readrops app on my phone.

The rise, fall, and resurgence of RSS/Atom

In the early days, RSS was everywhere - blogs and news sites all used it, and podcasts did too. Most people, including me, relied on feed readers to keep up with their favorite sites. Feeds were a simple and effective way to follow online content.

Then social platforms came along. They promised to show us content based on our interests using recommendation algorithms. When Google Reader (a prominent feed reader at the time) shut down in 2013, it marked a shift in how we read online. Like many others, I switched to getting my news and updates from social media.

After seeing how social media algorithms favor engagement over user interests, some of us started going back to RSS. While it's not perfect, I prefer the straightforward approach of choosing what I want to read rather than having it chosen for me.

Adding feeds to my blog

Since I started using RSS feeds again, I wanted my Nuxt 3 blog to support both feed formats. I went with both formats because some feed readers are picky. Since the library generates the feeds, the extra few lines of code don't add much.

Here's how I added RSS and Atom feeds to my blog:

Setting up the feed package

The first step was installing the feed package:

npm install feed
# or with yarn
yarn add feed

Creating the feed system

Fetching blog posts

I started by creating a function to fetch my blog posts. Here's what I put in server/utils/blog.ts:

import type { BlogPostPreview } from "~/types";

export async function getBlogPosts(event: any): Promise<BlogPostPreview[]> {
  try {
    // event parameter is required for server-side content querying
    return await queryCollection(event, "blog")
      .select("title", "description", "tags", "postDate", "path", "url")
      .order("postDate", "DESC")
      .all();
  } catch (e) {
    console.error("Error fetching blog posts:", e);
    return [];
  }
}

It took me a while to figure this out: on the server-side, we also need to pass the event parameter to queryCollection to access the server request context. Client-side code doesn't need this.

Generating the feeds

Next, I needed functions to generate the actual feeds. Here's what I created in server/utils/feed.ts:

// Import Feed library and our blog post type
import { Feed } from "feed";
import type { BlogPostPreview } from "~/types";

export type FeedFormat = "rss" | "atom";

// Default feed metadata - customize these values
const DEFAULT_TITLE = "Your Site Name";
const DEFAULT_DESCRIPTION = "Your site description";

// Creates a new Feed instance with common configuration
export function createBaseFeed(baseUrl: string) {
  if (!baseUrl) {
    throw new Error("Base URL is required to create a feed");
  }

  return new Feed({
    title: DEFAULT_TITLE,
    id: baseUrl,
    link: baseUrl,
    description: DEFAULT_DESCRIPTION,
    language: "en",
    copyright: `Copyright ${new Date().getFullYear()} Your Name`,
    // Advertise both feed formats
    feedLinks: {
      rss2: `${baseUrl}/rss.xml`,
      atom: `${baseUrl}/atom.xml`,
    },
    author: {
      name: DEFAULT_TITLE,
      link: baseUrl,
    },
    updated: new Date(), // Required for Atom feeds
  });
}

// Adds blog posts to the feed with proper metadata
export function addPostsToFeed(
  feed: Feed,
  posts: BlogPostPreview[],
  baseUrl: string,
) {
  if (!feed || !Array.isArray(posts) || !baseUrl) {
    throw new Error("Feed, posts array, and base URL are required");
  }

  for (const post of posts) {
    // Skip posts without required fields
    if (!post.title || !post.path) {
      console.warn("Skipping invalid post:", post);
      continue;
    }

    // Add post to feed with full metadata
    feed.addItem({
      title: post.title,
      id: `${baseUrl}${post.path}`, // Unique identifier
      link: `${baseUrl}${post.path}`, // Full URL to post
      description: post.description || "",
      date: new Date(post.postDate || Date.now()),
      category: post.tags?.map((tag) => ({ name: tag })) || [],
      author: [
        {
          name: DEFAULT_TITLE,
          link: baseUrl,
        },
      ],
    });
  }
}

// Unified feed generation function
export async function generateFeed(
  event: any,
  baseUrl: string,
  format: FeedFormat,
): Promise<string> {
  try {
    const blogPosts = await getBlogPosts(event);
    const feed = createBaseFeed(baseUrl);
    addPostsToFeed(feed, blogPosts, baseUrl);

    return format === "rss" ? feed.rss2() : feed.atom1();
  } catch (error) {
    console.error(`Error generating ${format} feed:`, error);
    throw error;
  }
}

Setting up the routes

With the utility functions in place, I created two route handlers - one for RSS and one for Atom:

In server/routes/rss.xml.ts:

import { generateFeed } from "../utils/feed";

export default defineEventHandler(async (event) => {
  const baseUrl = "https://subodhdahal.com";
  const feedContent = await generateFeed(event, baseUrl, "rss");

  setResponseHeader(
    event,
    "content-type",
    "application/rss+xml; charset=utf-8",
  );
  return feedContent;
});

In server/routes/atom.xml.ts:

import { generateFeed } from "../utils/feed";

export default defineEventHandler(async (event) => {
  const baseUrl = "https://subodhdahal.com";
  const feedContent = await generateFeed(event, baseUrl, "atom");

  setResponseHeader(
    event,
    "content-type",
    "application/atom+xml; charset=utf-8",
  );
  return feedContent;
});

My RSS feed is available at /rss.xml and my Atom feed at /atom.xml.

Making the feeds discoverable

To help feed readers find my feeds automatically, I added these links to my app.vue:

useHead({
  link: [
    {
      rel: "alternate",
      type: "application/rss+xml",
      title: "Your Site Name (RSS)",
      href: "/rss.xml",
    },
    {
      rel: "alternate",
      type: "application/atom+xml",
      title: "Your Site Name (Atom)",
      href: "/atom.xml",
    },
  ],
});

Making feeds visible to users

I put RSS and Atom feed links in my website's footer with a familiar RSS icon. Visitors can find and subscribe to my feeds through their preferred reader.

Static site generation

Since my blog is static, I needed to generate the feeds at build time. I added this to my nuxt.config.ts:

export default defineNuxtConfig({
  nitro: {
    prerender: {
      routes: ["/rss.xml", "/atom.xml"],
    },
  },
  // ... other config
});

Testing the feeds

After setting everything up, I tested my feeds by:

  1. Building and deploying the site
  2. Checking both /rss.xml and /atom.xml URLs
  3. Validating the feeds with online tools like the W3C Feed Validator
  4. Subscribing to them in my own FreshRSS instance

Conclusion

Adding RSS and Atom feeds to my Nuxt 3 blog took a library and a few short files. Now my readers can follow my updates in their preferred feed readers, just like I follow other blogs.

The full code is in my GitHub repository.