Adding an RSS Feed to a Next.js Blog
In my last post, I talked a bit about migrating this blog over from Gatsby to Next.js. One thing I had to
sort out in that migration was moving from the relatively-straightforward implementation of gatsby-plugin-feed
to building a custom-rolled RSS feed.
It actually turned out to be a relatively painless process, so I thought I'd share how it works.
A Bit of Background
First, it's probably helpful to cover how I've got things set up. I write everything in markdown (mdx) and have it sitting in a content
directory,
alongside the src
and public
directories in my codebase.
Here's roughly the file structure we'll be working with in this tutorial:
blog/
├─ src/
│ ├─ pages/
│ │ ├─ api/
│ │ │ ├─ rss/
│ │ │ │ ├─ index.ts
│ │ │ │ └─ feed.json
├─ scripts/
│ └─ rss.mjs
├─ content/
│ └─ blog-post.mdx
└─ package.json
The way that this will end up working is that we will use a prebuild
hook in our package.json
to run a node script that will grab all of our posts
and format them into a feed.json
file. From there we'll create an API route that will use that json file to serve up our RSS feed. Lastly, we'll use our
next.config.js
file to add a rewrite, so that we can serve the RSS feed from /rss.xml
.
The Node Script
This node script will be run on a prebuild
hook. When we build our site to deploy, the script will run through the content/
directory, grab all of our posts
loop through them and format them into a feed.json
file that we can deploy alongside our API endpoint.
The primary reason for generating this JSON file is so that we don't have to do a bunch of file system reads when someone requests the RSS feed.
Here's the script in full:
// scripts/rss.mjs
import fs from 'fs';
import matter from 'gray-matter';
import path from 'path';
const generateRss = async () => {
const POSTS_DIR = path.join(process.cwd(), 'content');
const posts = fs
.readdirSync(POSTS_DIR)
.filter((filePath) => filePath !== '.DS_Store') // only needed for macOS
.map((filePath) => {
const source = fs.readFileSync(path.join(POSTS_DIR, filePath));
const { content, data } = matter(source);
return {
content,
data,
filePath,
};
})
.sort(
(a, b) =>
new Date(b.data.date).getTime() - new Date(a.data.date).getTime(),
);
// Create sitemap file
fs.writeFileSync('src/pages/api/rss/feed.json', JSON.stringify(posts));
};
generateRss();
The way this works is that we're calling readdirSync
on the directory that contains all of our mdx files to get an array of file names,
then filtering out the .DS_Store
file (only necessary for macOS), and then mapping over that list of files.
When we map over the array of file names, we then call readFileSync
to read the individual file and use the gray-matter
package to extract the
frontmatter into an object, and return all of that data to make our posts
variable an array of objects that contain all of our posts and their frontmatter.
Finally, we sort the posts by publish date and then call fs.writeFileSync
to write a feed.json
file into the pages/api/rss
directory; where it'll sit next
to the API endpoint handler.
The Prebuild Hook
Not much to this one. When you run npm run build
, the prebuild
hook will run before the build process begins. We'll use this hook to run the node script we just built.
{
"scripts": {
"generate:rss": "node scripts/rss.mjs",
"prebuild": "npm run generate:rss"
}
}
The API Route
This is the meat and potatoes of serving up the RSS feed. We're using a custom API route to run a lambda function that loads up the JSON file that we generated in the previous step, formats it into an XML document, and returns it back to the requester.
// src/pages/api/rss/index.ts
import { NextApiRequest, NextApiResponse } from 'next';
import nc from 'next-connect';
import { BlogPost } from 'lib/types';
import feed from './feed.json';
const metadata = {
title: 'JonBellah.com',
description: 'About my site!',
link: 'https://jonbellah.com'
}
const handler = nc();
/**
* Respond with an rss.xml
*
* @param {object} req NextApiRequest
* @param {object} res NextApiResponse
*/
handler.get(async (req: NextApiRequest, res: NextApiResponse) => {
try {
const postItems = feed
.map((page: BlogPost) => {
const url = `${
process.env.NEXT_PUBLIC_ROOT_URL
}/articles/${page.filePath.replace('.mdx', '')}`;
return `<item>
<title>${page.data.title}</title>
<link>${url}</link>
<guid>${url}</guid>
<pubDate>${page.data.date}</pubDate>
${
page.data.excerpt &&
`<description>${page.data.excerpt}</description>`
}
${/**
* NOTE: page.content on the next line should be wrapped with
* the CDATA tag, but it breaks my _actual_ RSS feed if I
* include it. Check the link to the source at the bottom
* of this post for the actual code.
*/}
<content:encoded>${page.content}</content:encoded>
</item>`;
})
.join('');
// Add urlSet to entire sitemap string
const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0">
<channel>
<title>${metadata.title}</title>
<description>${metadata.description}</description>
<link>${metadata.link}</link>
<lastBuildDate>${feed[0].data.date}</lastBuildDate>
${postItems}
</channel>
</rss>`;
// set response content header to xml
res.setHeader('Content-Type', 'text/xml');
return res.status(200).send(sitemap);
} catch (e: unknown) {
if (!(e instanceof Error)) {
throw e;
}
return res.status(500).json({ error: e.message || '' });
}
});
export default handler;
It's worth noting, too, that if you're using a CMS that has a REST API — like WordPress — you can fetch the list of posts in this
lambda function and build your RSS feed on-the-fly. If your content is stored in an external CMS, you can skip the prebuild
script and just use the API route.
Another thing worth noting is that the url
variable within the postItems
map assumes that the path for posts will be articles/{post-slug}
. Be sure to change that to
whatever path you prefer for your own site.
Configure the Next Rewrite
The final step to get this up and running is to use our next.config.js
file to add a rewrite, so that we can serve the RSS feed from /rss.xml
instead of /api/rss
.
Feel free to make the source
route whatever you prefer, especially if you're trying to preserve the route to an existing RSS feed.
// next.config.js
module.exports = {
rewrites: async () => [
{
source: '/rss.xml',
destination: '/api/rss',
},
],
};
Now once you deploy your site to Vercel, you can visit yoursite.com/rss.xml
and see your RSS feed!
Source
You can find the source code for this tutorial here.