Next.js: RSS with Static Site Generation

In this post I want to share my configuration for generating RSS feeds for my statically generated Next.js site.

I still love RSS feeds as a way of keeping in touch with subjects and people. It's like a timeline that doesn't move quite as quickly and a convenient way to actually engage with awesome content people put out, when I actually have the time for it. Of course I was going to keep having a feed after migrating my blog from WordPress to Next.js.

The most important module for this is npmjs.org/feed, which will do all the heavy lifting for us. We just need to provide it a few fields and then push our articles to an array that will be transformed into:

  • rss.xml
  • atom.xml
  • feed.json (Yes, we can have "rss" in JSON now!)

The default example on their README will also get us most of the way there, except we still need to load our posts. If you have followed any of the Next.js tutorials or examples on how to build a blog you probably have all of these things available:

  • title
  • content
  • slug (this might be a filename for you, cut off the .mdx)

I have a function that loads all of my posts from disk into memory (because I split my posts into years), so I have a slug field in my frontmatter.

Example frontmatter from a post:

---
title: Nginx 410 maps and Custom Error Page
date: '2022-03-29'
thumbnailURL: '/assets/2022/nginx-410-map.png'
slug: 'nginx-410-maps-and-custom-error-page'
tags: ['nginx', 'seo']
---

Alright, let's assume we have all that (and maybe some content) available for each blog post or article and set up our feed. You only need to build your feeds once per build, so I run my generateFeeds function on my /archive page (which lists ALL posts anyways).

import fs from "fs";
import { Feed } from "feed";
import constants from './constants';

const generateFeeds = async (posts) => {
	const { baseURL, author, feedDirectory } = constants;

	const feed = new Feed({
		title: `JonathanMH`,
		description: "Code, Photography and other adventures",
		id: baseURL,
		link: baseURL,
		language: "en",
		generator: "Next.js using Feed for Node.js",
		feedLinks: {
			rss2: `${baseURL}/${feedDirectory}/feed.xml`,
			atom: `${baseURL}/${feedDirectory}/atom.xml`,
			json: `${baseURL}/${feedDirectory}/feed.json`,
		},
		author,
	});
	
	posts.forEach((post) => {
		const { title, slug, description, date } = post.frontMatter;
		// blog posts can be found at /p/post-slug
		const url = `${baseURL}/p/${slug}`;
		feed.addItem({
			title: title,
			id: url,
			link: url,
			content: description,
			author: [author],
			date: new Date(date)
		});
	});
 
	// we use this to make sure the directory exists
	fs.mkdirSync(`./public/${feedDirectory}`, { recursive: true });
	fs.writeFileSync(`./public/${feedDirectory}/feed.xml`, feed.rss2());
	fs.writeFileSync(`./public/${feedDirectory}/atom.xml`, feed.atom1());
	fs.writeFileSync(`./public/${feedDirectory}/feed.json`, feed.json1());
}

export default generateFeeds;

This will generate three different feeds and save them to the correct directory.

Some differences to the example from the feed module are some constants I have defined, baseURL and feedDirectory which are https://jonathanmh.com and feed. This makes it easier for me to test it in different environments or to change where all feeds are written to. This still leaves me with a feed URL that's different than what I had in WordPress. There my RSS feed was available at https://jonathanmh.com/feed. I'm solving this with a simple nginx redirect:

rewrite /feed$ /feed/feed.xml permanent;

Perfect, we have some feeds, but we need to make them discoverable as well!

Listing RSS feeds in link tags

I learned that you can write some meta tags to tell the client about RSS feeds on a domain by linking them in the <head> portion of your HTML:

<link rel="alternate" type="application/rss+xml" title="RSS" href="https://jonathanmh.com/feed/feed.xml">

I decided that that's probably not too much overhead to add to every page and added the reference. We can create a component for meta/head tags for every page or limit them to specific pages depending on where you want to render the additional tags. I user this component:

import Head from "next/head"
// Look at constants coming in handy again
import constants from "../../lib/constants";

const GlobalMeta = () => {
	const { feedDirectory, baseURL } = constants;
	return (
		<Head>
			<meta name="viewport" content="width=device-width, initial-scale=1" />
			<link rel="alternate" type="application/rss+xml" title="RSS" href={`${baseURL}/${feedDirectory}/feed.xml`} />
			<link rel="alternate" type="application/rss+xml" title="Atom" href={`${baseURL}/${feedDirectory}/atom.xml`} />
			<link rel="alternate" type="application/json" title="JSON Feed" href={`${baseURL}/${feedDirectory}/feed.feed.json`} />
		</Head>
	)
}

export default GlobalMeta;

and import it in my _app.js file:

import GlobalMeta from '../components/Meta/Global'

function MyApp({ Component, pageProps }) {
	return <div className='container'>
		<GlobalMeta />
		<main className='content'>
			<Component {...pageProps} />
		</main>
	</div>
}

I haven't decided how to link to the RSS feed from the visible part of the page yet, because linking to an XML file is really boring, but I also don't want to favour a specific RSS reader. If you have any great advice for this, let me know!

Limit your RSS feed length

You can always trim your RSS feed by passing only a limited number of posts to the function that creates the feeds, like the last 20 or 100. We need to pay attention to the order of the array though, because you probably want the most recent 20 posts and not the ones that were found on disk first or similar.

Example for sorting by date and only using the 20 most recent posts.

	// sort most recent first
	posts = posts.sort(function (postA, postB) {
		const postADate = DateTime.fromSQL(postA.frontMatter.date);
		const postBDate = DateTime.fromSQL(postB.frontMatter.date);
		return postBDate.toMillis() - postADate.toMillis();
	}).slice(0, 20);
Tagged with: #Next.js #RSS

Thank you for reading! If you have any comments, additions or questions, please tweet or toot them at me!