Route360

RSS feed for Next.js multilingual Markdown blog

Table of Contents

I finally succeeded in the installation of RSS feeds for this blog (route360.dev).

I know it's not complicated for a monolingual blog, but I write in three languages (English, French, and Japanese). The feeds would need to be generated for each language, which was a bit complicated.

For the codes, the following blog helped a lot. Thank you🙏

Reference Next.jsにfeedを導入してRSSとAtomのフィードを生成しよう

My codes are arranged for multi-language based on his.

Working environment:

  • Node.js v16.18.0
  • React v18.2.0
  • Next.js v12.3.1
  • feed v4.2.2
  • marked v4.1.1

Outline

  1. How to optimize different URLs of posts
  2. How to send the posts list of each language to the RSS feed generator function

1 -> Though it's normal that the title changes by language, URLs changes as well; in the URL, no language code for the default language but others such as /ja/ (in case of Sub-path Routing). To manage this problem, let the feed function receive two arguments - locale and posts - so that it can generate feeds separately for each language.

2 -> The posts list is already generated at the blog top page, which we can use for the feeds. Insert the feed function into getStaticProps of the blog top page by adding the locale and the posts as the arguments.

Install feed

First, install feed, a Node.js package.

## for npm
npm install feed

## for yarn
yarn add feed

Create an RSS feed function component

The basic information about the website

I prepared the basic information about the website like the following;

const siteTitle = {
  en: "My Great Website!",
  fr: "Mon site web superbe !",
  ja: "私のサイトは素晴らしい!",
}
const siteDesc = {
  en: "This is my finest website ever.",
  fr: "C'est mon site web le plus cool !",
  ja: "これ以上ない素晴らしすぎるサイトです。",
}
const siteUrl = "https://example.com/"
const defaultLocale = "en"
const author = "Tokugawa Ieyasu"
const email = "[email protected]"

This is just an example. In general, the basic information must be stored in /lib/constats.js or somewhere, and you can retrieve that info from it.

Generate the basic data for the RSS feeds

The basic information must be placed on the top of the hierarchy of the feeds. Let us create it first.

/lib/feed.js
import { Feed } from 'feed'

export default function GeneratedRssFeed(locale, posts) {
  const siteTitle = {
    en: "My Great Website!",
    fr: "Mon site web superbe !",
    ja: "私のサイトは素晴らしい!",
  }
  const siteDesc = {
    en: "This is my finest website ever.",
    fr: "C'est mon site web le plus cool !",
    ja: "これ以上ない素晴らしすぎるサイトです。",
  }
  const siteUrl = "https://example.com/"
  const defaultLocale = "en" // default locale
  const author = "Tokugawa Ieyasu"
  const email = "[email protected]"

  const feed = new Feed({
    title: `${siteTitle[locale]}`,
    description: siteDesc[locale],
    id: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`,
    link: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`,
    language: locale,
    image: `${siteUrl}image.png`,
    favicon: `${siteUrl}favicon.png`,
    copyright: `Copyright ${siteTitle} All rights reserved`,
    generator: "Feed for Node.js", // option (default: https://github.com/jpmonette/feed)
    updated: new Date(), // option (default: today)
    feedLinks: {
      json: `${siteUrl}rss/feed.${locale}.json`,
      rss2: `${siteUrl}rss/feed.${locale}.xml`,
      atom: `${siteUrl}rss/feed.${locale}.xml`,
    },
    author: {
      name: author,
      email: email,
      link: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`
    }
  })
}

The key of the code above is how to retrieve the data and how to branch it by each locale.

Because the function catches locale as one of the two arguments, the text can be switched with brackets [] with the locale inside (e.g. siteTitle[locale]).

Add the posts list into RSS feed

Next, we add the posts one by one into the RSS feed using .addItem().

In my environment, the conditions are;

  • Each post is generated by /pages/post/[slug].js => URL is something like https://example.com/post/my-post/ (for the non-default locales, /ja/ would be inserted).
  • Metadata (title,slug or date) of each post is managed by Frontmatter.

Here I use marked as a converter of the Markdown content, but it's of course up to you; you can use another library or module. It all depends on your environment of the condition.

/lib/feed.js
import { Feed } from 'feed'
import { marked } from "marked"

export default function GeneratedRssFeed(locale, posts) {
  //...
  const feed = new Feed({
    //...
  })

  posts.forEach(post => {
    feed.addItem({
      title: post.frontmatter.title,
      id: locale === defaultLocale
        ? `${siteUrl}post/${post.frontmatter.slug}/`
        : `${siteUrl}${locale}/post/${post.frontmatter.slug}/`,
      link: locale === defaultLocale
        ? `${siteUrl}post/${post.frontmatter.slug}/`
        : `${siteUrl}${locale}/post/${post.frontmatter.slug}/`,
      description: marked(post.content).slice(0, 120),
      content: marked(post.content),
      author: [
        {
          name: author,
          email: email,
          link: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`
        }
      ],
      date: new Date(post.frontmatter.date),
    })
  })
}

The URLs for link and id is separated according to the language.

Also, in this example, I generate a description with 120 characters and content for full text. Change them if you need.

Save RSS feeds for each locale

At last, save the RSS feeds into /public/rss/ directory with fs module.

Because RSS feeds must be generated and saved for each language, I added the locales just before the extensions (e.g. feed.en.xml).

/lib/feed.js
import fs from 'fs'
//...

export default function GeneratedRssFeed(locale, posts) {
  //...
  posts.forEach(post => {
    //...
  })

  fs.mkdirSync('./public/rss', { recursive: true })
  fs.writeFileSync(`./public/rss/feed.${locale}.json`, feed.json1())
  fs.writeFileSync(`./public/rss/feed.${locale}.xml`, feed.rss2())
  fs.writeFileSync(`./public/rss/atom.${locale}.xml`, feed.atom1())
}

Generate RSS feeds with getStaticProps of the blog top page

When generating the blog top page (= recent posts list page) with getStaticProps, generate the RSS feeds together.

In this blog (route360.dev), the blog's top page is displayed from /pages/index.js. Just insert the feed function into the inside of getStaticProps.

Because we are making a website by i18n of Next.js, getStaticProps receives { locale } as a context. This locale can be sent as the first argument of GeneratedRssFeed().

  • 1st argument: locale
  • 2nd argument: list of the posts

In this example, the posts list includes the latest five posts using slice().

/pages/index.js
import GeneratedRssFeed from 'lib/feed'

//...

export async function getStaticProps({ locale }) {
  //...
  GeneratedRssFeed(locale, posts.sort(sortByDate).slice(0, 5))
  //...
}

sortByDate is a custom function that was used in "How I built this site with Next.js + Markdown + i18n"; this function sorts the post by date.

You are almost. When index.js gets access from a browser, the RSS feeds for the displayed language should be generated under the /public/rss directory.

Add rss directory to .gitignore

To avoid conflicts in production, add the /public/rss directory in .gitignore.

/.gitignore
/public/rss

Register the feeds on Google Search Console and Bing Webmaster Tools

Register the RSS feeds to Search Engines as you like.

The code (conclusion)

The final RSS feed function components are as below;

/lib/feed.js
import fs from 'fs'
import { Feed } from 'feed'
import { marked } from "marked"

export default function GeneratedRssFeed(locale, posts) {
  const siteTitle = {
    en: "My Great Website!",
    fr: "Mon site web superbe !",
    ja: "私のサイトは素晴らしい!",
  }
  const siteDesc = {
    en: "This is my finest website ever.",
    fr: "C'est mon site web le plus cool !",
    ja: "これ以上ない素晴らしすぎるサイトです。",
  }
  const siteUrl = "https://example.com/"
  const defaultLocale = "en" // default locale
  const author = "Tokugawa Ieyasu"
  const email = "[email protected]"

  const feed = new Feed({
    title: `${siteTitle[locale]}`,
    description: siteDesc[locale],
    id: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`,
    link: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`,
    language: locale,
    image: `${siteUrl}image.png`,
    favicon: `${siteUrl}favicon.png`,
    copyright: `Copyright ${siteTitle} All rights reserved`,
    generator: "Feed for Node.js", // option (default: https://github.com/jpmonette/feed)
    updated: new Date(), // option (default: today)
    feedLinks: {
      json: `${siteUrl}rss/feed.${locale}.json`,
      rss2: `${siteUrl}rss/feed.${locale}.xml`,
      atom: `${siteUrl}rss/feed.${locale}.xml`,
    },
    author: {
      name: author,
      email: email,
      link: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`
    }
  })

  posts.forEach(post => {
    feed.addItem({
      title: post.frontmatter.title,
      id: locale === defaultLocale
        ? `${siteUrl}post/${post.frontmatter.slug}/`
        : `${siteUrl}${locale}/post/${post.frontmatter.slug}/`,
      link: locale === defaultLocale
        ? `${siteUrl}post/${post.frontmatter.slug}/`
        : `${siteUrl}${locale}/post/${post.frontmatter.slug}/`,
      description: marked(post.content).slice(0, 120),
      content: marked(post.content),
      author: [
        {
          name: author,
          email: email,
          link: locale === defaultLocale ? siteUrl : `${siteUrl}${locale}/`
        }
      ],
      date: new Date(post.frontmatter.date),
    })
  })

  fs.mkdirSync('./public/rss', { recursive: true })
  fs.writeFileSync(`./public/rss/feed.${locale}.json`, feed.json1())
  fs.writeFileSync(`./public/rss/feed.${locale}.xml`, feed.rss2())
  fs.writeFileSync(`./public/rss/atom.${locale}.xml`, feed.atom1())
}

It's done! It was a bit tiring, though.😕

RSS feeds don't affect SEO directly, but they would increase the number of crawlers and the rate of repeat visitors.