DEV Community

Kevin Langley Jr
Kevin Langley Jr

Posted on • Originally published at kevinlangleyjr.dev on

Generating an RSS Feed and Sitemap in Next.js

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

# Using yarn
yarn add feed -D

# or using NPM
npm install feed --save-dev
Enter fullscreen mode Exit fullscreen mode

Implementation

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

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 () => {

}
Enter fullscreen mode Exit fullscreen mode

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.

// ...

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',
  };
}

// ...
Enter fullscreen mode Exit fullscreen mode

Then I created a new Feed object.

// ...

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,
} );

// ...
Enter fullscreen mode Exit fullscreen mode

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.

// ...

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 ),
    }
);

}

// ...
Enter fullscreen mode Exit fullscreen mode

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

// ...

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() );

// ...
Enter fullscreen mode Exit fullscreen mode

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.

// ...

export const getStaticProps: GetStaticProps = async () => {
  const { posts, hasNextPage } = getPostsForPage();

  if ( process.env?.VERCEL ) {
    await generateFeeds();
  }

  return {
    props: {
      posts,
      hasNextPage,
    },
  };
};
// ...
Enter fullscreen mode Exit fullscreen mode

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

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() );
}
Enter fullscreen mode Exit fullscreen mode

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

# Using yarn
yarn add next-sitemap -D

# or using NPM
npm install -D next-sitemap
Enter fullscreen mode Exit fullscreen mode

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

module.exports = {
 siteUrl: `https://${ process.env.VERCEL\_URL }`,
 generateRobotsTxt: true,
};
Enter fullscreen mode Exit fullscreen mode

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

{
  "build": "next build",
  "postbuild": "next-sitemap"
}
Enter fullscreen mode Exit fullscreen mode

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! ๐ŸŽ‰๐ŸŽ‰๐ŸŽ‰

Top comments (0)