December 29, 2020

How to add an RSS feed to a Next.js site

RSS feeds are a useful resource for being able to follow aggregated updates across all your favorite blogs, podcasts, and other digital medium. Thankfully, it's actually pretty easy to get set up, especially on a Next.js site.

Gathering Requirements

An RSS feed is just a special XML description of the content on the site. You can read the specification here if you're interested.

  1. Create the RSS feed
      • As a bonus this could include Atom and JSON feeds.
  1. Expose the feed(s) through a URL
  1. Add meta links in the header for RSS discoverability

Creating the RSS endpoints

The first decision is where to serve the RSS (and related feeds) from. After a little research I learned that Wordpress uses /feed as the default route to serve the feed, which seems good enough for me. In that case, the routes I want to expose are:

  • GET /feed , GET /feed/rss → RSS Feed
  • GET /feed/atom → Atom Feed
  • GET /feed/json → JSON Feed

You can choose different paths than this if you'd like, I don't think the actual route is that important.

These routes server non-html (data) payloads so in my mind they map well to Next.js API routes. Given that, I decided to create them under pages/api/feed/*. That ultimately ends up with me having these files:

  • pages/api/feed/rss.ts
  • pages/api/feed/atom.ts
  • pages/api/feed/json.ts

Given how Next.js works though, the above API routes won't be at the path I want! The RSS feed will be at /api/feed/rss and I want it to serve from /feed. Thankfully in Next.js 9.5 the Vercel team introduced rewrites to solve for this.

Rewrites allow you to map an incoming request path to a different destination path. — Next.js Docs
// next.config.js
module.export = {
	async rewrites() {
	    return [
	      {
	        source: "/feed",
	        destination: "/api/feed/rss",
	      },
	      {
					// The /:slug part is a generic parameter handler to catch all other cases
	        source: "/feed/:slug",
	        destination: "/api/feed/:slug",
	      },
	    ];
	  }
}

Setting up the feed

For expediencies' sake I chose to use the feed module to build out my feed and its different formats. This module gives a single, typed interface for exporting all the types of feeds that I'm interested in.

// lib/feed.ts
import { Feed } from "feed";
import { getPublishedPosts } from "./notion/blog";

export const buildFeed = async () => {
	// This contains site level metadata like title, url, etc
  const feed = new Feed({
    // Global feed config
  });

  const posts = await getPublishedPosts();
  posts.forEach((post) => {
    feed.addItem({
      // Individual post config
    });
  });

  return feed;
};

It's worth noting that this feed configuration is actually shared between all the different formats. That means we can set up the feed once and have it generate rss, atom, and json feeds accordingly.

Connecting the feed to our routes

Now that we have the feed defined, let's hook up the api routes to return our feed data.

// pages/api/feed/rss.ts
import { NextApiRequest, NextApiResponse } from "next";
import { buildFeed } from "lib/feed";

export default async (req: NextApiRequest, res: NextApiResponse) => {
  const feed = await buildFeed();

  res.statusCode = 200;
  res.setHeader("content-type", "application/rss+xml");
  res.end(feed.rss2());
};

That's all it takes to serve the RSS feed! The only real work here is setting the header for the appropriate response type and calling the appropriate formatting function from the feed module. If this were an atom feed the content-type header value would be application/atom+xml and the feed formatter feed.atom1(). Likewise for the json feed we'd have application/feed+json and feed.json1().

A note about content-type headers

There's actually a lot of different advice on what to set the content-type header for these. The JSON feed header is most correct as listed on the spec site. While application/rss+xml is technically the most correct MIME type for RSS, it's actually not the most compatible. If you try to open an RSS feed with that MIME type on Firefox for example, it prompts you to download the file instead of displaying it. W3 notes that for widest compatibility, it's okay to use application/xml. That said, a lot of sites even use text/xml. I'll likely end up updating mine to application/xml, I just haven't got around to that yet.

Adding feed discoverability

There's typically two things you should do to make your feed discoverable. One is adding a link ref="alternative" in the head which points to the feed. For Next.js this means using the next/head component.

// pages/index.tsx
import Head from 'next/head'

const Blog = () => {
  return (
    <>
			<Head>
        <link
          key="rss-feed"
          rel="alternative"
          type="application/rss+xml"
          title="RSS feed for just-be.dev"
          href="/feed"
        />
        <link
          key="atom-feed"
          rel="alternative"
          type="application/atom+xml"
          title="Atom feed for just-be.dev"
          href="/feed/atom"
        />
        <link
          key="json-feed"
          rel="alternative"
          type="application/feed+json"
          title="JSON feed for just-be.dev"
          href="/feed/json"
        />
      </Head>
			{/* other page content here */}
    </>
  )
}

Some browsers/extensions will give users indicators to subscribe to the feed when this is discovered. This will also give a hint to crawlers that a feed is available.

The other thing you should do to make your feed discoverable is pretty simple... add a link to it on your page! Josh Comeau has a subtle icon in the top level of his site that points to it.

 
An example of an RSS feed button from Josh Comeau's site (it's on the right)
An example of an RSS feed button from Josh Comeau's site (it's on the right)

I referenced his site because I don't have one yet 😄. I'll get one up eventually, after I figure out how to rework my menu..

I hope this was helpful! If you have any questions, hit me up on twitter @zephraph. Also feel free to checkout out the source to see exactly what I've done.