Generating an RSS Feed and Sitemap in Next.js

📖 6 min read

With my new site being built with Next.js, I needed to figure out a way to still provide RSS feeds and a sitemap using the MDX files as a source. I use next-mdx-remote from Hashicorp for the parsing and rendering of my MDX files and with the latest major version, v3, it got to be a little tricky so hopefully you'll find this helpful!

RSS Feed

To generate an RSS feed for my Next.js site, I used the aptly named feed package and installed it as a dev dependency.

Installation

bash
# Using yarn
yarn add feed -D
# or using NPM
npm install feed --save-dev

Implementation

I then created a file named feeds.tsx in my utils directory. And we'll start off with this:

utils/feed.tsx
tsx
import fs from 'fs';
import { Feed } from 'feed';
import { serialize } from 'next-mdx-remote/serialize';
import { MDXRemote } from 'next-mdx-remote';
import { renderToStaticMarkup } from 'react-dom/server';
import { MDXComponents } from '@components/MDX';
import { getPosts } from '@utils/mdx';
const generateFeeds = async () => {
}

From there, I start with some basic data like the date, my name and author information, as well as the site url and I also fetch the posts which are just local MDX files in the data directory.

utils/feed.tsx
tsx
// ...
const generateFeeds = async () => {
const domain = process.env.NEXT_PUBLIC_MAIN_DOMAIN || process.env.VERCEL_URL;
const posts = await getPosts();
const siteURL = `https://${domain}`;
const date = new Date();
const author = {
name: 'Kevin Langley Jr.',
email: 'me@kevinlangleyjr.com',
link: 'https://kevinlangleyjr.dev',
};
}
// ...

Then I created a new Feed object.

utils/feed.tsx
tsx
// ...
const feed = new Feed( {
title: 'Kevin Langley | Web Engineer',
description: '',
id: siteURL,
link: siteURL,
image: `${siteURL}/kevinlangleyjr.jpg`,
copyright: `All rights reserved ${date.getFullYear()}, Kevin Langley Jr.`,
updated: date,
generator: 'Next.js',
feedLinks: {
rss2: `${siteURL}/rss/feed.xml`,
json: `${siteURL}/rss/feed.json`,
atom: `${siteURL}/rss/atom.xml`,
},
author,
} );
// ...

Then we can finally start looping over our posts to add to the feed. With the latest changes to the next-mdx-remote package and no longer having the renderToString method in v3, we are using renderToStaticMarkup from react-dom/server to generate the markup to add to our feeds.

utils/feed.tsx
tsx
// ...
for ( const post of posts ) {
const url = `${siteURL}/blog/${post.slug}`;
const { content, ...postData } = post;
const mdxSource = await serialize( content, {
scope: postData as Record<string, any>, // Scope expects a <Record> here.
mdxOptions: {
remarkPlugins: [
// I use a few different remark plugins, check out my "How I built my blog" article for more information!
],
},
} );
const markup = renderToStaticMarkup(
<MDXRemote components={ MDXComponents } { ...mdxSource } />
);
feed.addItem(
{
title: post.title,
guid: post.slug,
id: url,
link: url,
description: post.excerpt,
content: markup,
author: [ author ],
contributor: [ author ],
date: new Date( post.published_date ),
}
);
}
// ...

And then we can finally write those feeds to their appropriate files.

utils/feed.tsx
tsx
// ...
fs.mkdirSync( './public/rss', { recursive: true } );
fs.writeFileSync( './public/rss/feed.xml', feed.rss2() );
fs.writeFileSync( './public/rss/atom.xml', feed.atom1() );
fs.writeFileSync( './public/rss/feed.json', feed.json1() );
// ...

Now, how do we get these feeds to automatically run on build?

Easy! Just call it in the getStaticProps method in the blog index and it will generate on build. I like to wrap it in a check for the VERCEL environment variable which will then only run when building on the Vercel platform, or if I pass in the environment variable when building locally.

pages/blog/index.ts
tsx
// ...
export const getStaticProps: GetStaticProps = async () => {
const { posts, hasNextPage } = getPostsForPage();
if ( process.env?.VERCEL ) {
await generateFeeds();
}
return {
props: {
posts,
hasNextPage,
},
};
};
// ...

Here is the entire feed.tsx file with all the steps completed.

utils/feed.tsx
tsx
import fs from 'fs';
import { Feed } from 'feed';
import { serialize } from 'next-mdx-remote/serialize';
import { MDXRemote } from 'next-mdx-remote';
import { renderToStaticMarkup } from 'react-dom/server';
import { MDXComponents } from '@components/MDX';
import { getPosts } from '@utils/mdx';
const generateFeeds = async () => {
const domain = process.env.NEXT_PUBLIC_MAIN_DOMAIN || process.env.VERCEL_URL;
const posts = await getPosts();
const siteURL = `https://${domain}`;
const date = new Date();
const author = {
name: 'Kevin Langley Jr.',
email: 'me@kevinlangleyjr.com',
link: 'https://kevinlangleyjr.dev',
};
const feed = new Feed( {
title: 'Kevin Langley | Web Engineer',
description: '',
id: siteURL,
link: siteURL,
image: `${siteURL}/kevinlangleyjr.jpg`,
copyright: `All rights reserved ${date.getFullYear()}, Kevin Langley Jr.`,
updated: date,
generator: 'Next.js',
feedLinks: {
rss2: `${siteURL}/rss/feed.xml`,
json: `${siteURL}/rss/feed.json`,
atom: `${siteURL}/rss/atom.xml`,
},
author,
} );
for ( const post of posts ) {
const url = `${siteURL}/blog/${post.slug}`;
const { content, ...postData } = post;
const mdxSource = await serialize( content, {
scope: postData as Record<string, any>, // Scope expects a <Record> here.
mdxOptions: {
remarkPlugins: [
// I use a few different remark plugins, check out my "How I built my blog" article for more information!
],
},
} );
const markup = renderToStaticMarkup(
<MDXRemote components={ MDXComponents } { ...mdxSource } />
);
feed.addItem(
{
title: post.title,
guid: post.slug,
id: url,
link: url,
description: post.excerpt,
content: markup,
author: [author],
contributor: [author],
date: new Date(post.published_date),
}
);
}
fs.mkdirSync( './public/rss', { recursive: true } );
fs.writeFileSync( './public/rss/feed.xml', feed.rss2() );
fs.writeFileSync( './public/rss/atom.xml', feed.atom1() );
fs.writeFileSync( './public/rss/feed.json', feed.json1() );
}

Sitemap

For the sitemap functionality, I chose to use next-sitemap which provides a simple API to create a sitemap from your static, dynamic, and server side pages in Next.js.

I'm only using static pages on my site, but you can check out the README if you're interested in generating dynamic or server-side sitemaps.

Installation

bash
# Using yarn
yarn add next-sitemap -D
# or using NPM
npm install -D next-sitemap

Implementation

From there, I created a new file in the root of the project named, next-sitemap.js and I provided a basic configuration within. You can find the different configuration options supported in the README

next-sitemap.js
tsx
module.exports = {
siteUrl: `https://${ process.env.VERCEL_URL }`,
generateRobotsTxt: true,
};

And then in my package.json, I added next-sitemap to the postbuild script.

package.json
tsx
{
"build": "next build",
"postbuild": "next-sitemap"
}

From there, whenever you build your project you'll have a sitemap.xml and if you set generateRobotsTxt to true in the configuration, a robots.txt generated into your public directory.

Profit! 🎉🎉🎉