Route360

Creating language-specific RSS feeds for a multilingual Gatsby site

Table of Contents

This website is developed with Gatsby.js (as of June 2023). The content is written in Markdown.

RSS feeds are created for each language.

I generate these RSS feeds with gatsby-plugin-feed, an official Gatsby plugin. Although this plugin is very useful, you can only generate one RSS for all languages with the official example.

In this entry, I'll show you how to generate RSS feeds for each locale.

Since I'm keeping the GitHub repository for this blog public, check it out if you're interested.

Environment:

  • gatsby v5.10.0
  • gatsby-plugin-feed v5.10.0
  • react v18.2.0
  • node v18.16.0

Prerequisite

The file structure of this site is as follows;

src/
โ”œโ”€ content/
| โ””โ”€ posts/
|    โ”œโ”€ first-post/
|    |    โ”œโ”€ en.md
|    |    โ”œโ”€ fr.md
|    |    โ””โ”€ ja.md
|    โ”œโ”€ second-post/
|    |    โ”œโ”€ en.md
|    |    โ”œโ”€ fr.md
|    |    โ””โ”€ ja.md

These dirnames/filenames are used for URL paths;

  • directory name = slug
  • filename = language code

To get these names as elements of markdownRemark in GraphQL, I added the following code to gatsby-node.js;

gatsby-node.js
exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === "MarkdownRemark") {
    const fileNode = getNode(node.parent)

    createNodeField({
      node,
      name: "language",
      value: fileNode.name,
    })

    createNodeField({
      node,
      name: "slug",
      value: fileNode.relativeDirectory.match(/\/(.+)/)[1],
    })
  }
}

You can now get its directory name as slug and the filename as language from the fields schema of markdownRemark.

query {
  markdownRemark {
    fields {
      language
      slug
    }
    frontmatter {
      ...
    }
  }
}

If you are using a CMS, you may be able to get the language code and slug from the default query. In these cases, edit the following code accordingly.

Code

Here is the code. It may look long, but I'm just repeating the same thing for each language.

*I have simplified my actual code for the sake of explanation.

gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-feed`,
      options: {
        query: `
          {
            site {
              siteMetadata {
                title
                siteUrl
              }
            }
          }
        `,
        feeds: [
          {
            query: `
              {
                allMarkdownRemark(
                  filter: {
                    fields: { language: { eq: "en" } }
                  }
                  sort: { frontmatter: { date: DESC } }
                  limit: 10
                ) {
                  nodes {
                    excerpt
                    frontmatter {
                      title
                      date
                    }
                    fields {
                      slug
                    }
                  }
                }
              }
            `,
            serialize: ({ query: { site, allMarkdownRemark } }) => {
              return allMarkdownRemark.nodes.map(node => {
                return Object.assign({}, node.frontmatter, {
                  description: node.excerpt,
                  date: node.frontmatter.date,
                  url: `${site.siteMetadata.siteUrl}/en/post/${node.fields.slug}/`,
                  guid: `${site.siteMetadata.siteUrl}/en/post/${node.fields.slug}/`,
                })
              })
            },
            output: "/rss.en.xml",
            title: "Route360",
            description: "Blog by a frontend developer",
            site_url: "https://route360.dev/en/",
            feed_url: "https://route360.dev/rss.en.xml",
          },
          {
            query: `
              {
                allMarkdownRemark(
                  filter: {
                    fields: { language: { eq: "fr" } }
                  }
                  sort: { frontmatter: { date: DESC } }
                  limit: 10
                ) {
                  nodes {
                    excerpt
                    frontmatter {
                      title
                      date
                    }
                    fields {
                      slug
                    }
                  }
                }
              }
            `,
            serialize: ({ query: { site, allMarkdownRemark } }) => {
              return allMarkdownRemark.nodes.map(node => {
                return Object.assign({}, node.frontmatter, {
                  description: node.excerpt,
                  date: node.frontmatter.date,
                  url: `${site.siteMetadata.siteUrl}/fr/post/${node.fields.slug}/`,
                  guid: `${site.siteMetadata.siteUrl}/fr/post/${node.fields.slug}/`,
                })
              })
            },
            output: "/rss.fr.xml",
            title: "Route360",
            description: "Blog par une dรฉveloppeuse front-end",
            site_url: "https://route360.dev/fr/",
            feed_url: "https://route360.dev/rss.fr.xml",
          },
          {
            query: `
              {
                allMarkdownRemark(
                  filter: {
                    fields: { language: { eq: "ja" } }
                  }
                  sort: { frontmatter: { date: DESC } }
                  limit: 10
                ) {
                  nodes {
                    excerpt
                    frontmatter {
                      title
                      date
                    }
                    fields {
                      slug
                    }
                  }
                }
              }
            `,
            serialize: ({ query: { site, allMarkdownRemark } }) => {
              return allMarkdownRemark.nodes.map(node => {
                return Object.assign({}, node.frontmatter, {
                  description: node.excerpt,
                  date: node.frontmatter.date,
                  url: `${site.siteMetadata.siteUrl}/ja/post/${node.fields.slug}/`,
                  guid: `${site.siteMetadata.siteUrl}/ja/post/${node.fields.slug}/`,
                })
              })
            },
            output: "/rss.ja.xml",
            title: "Route360",
            description: "ใƒ•ใƒญใƒณใƒˆใ‚จใƒณใƒ‰ใฎ้–‹็™บ่จ˜้Œฒ",
            site_url: "https://route360.dev/ja/",
            feed_url: "https://route360.dev/rss.ja.xml",
          },
        ],
      },
    },
  ],
}

RSS Feed Output Example

Here is an example of English RSS feed;

public/rss.en.xml
<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
   <channel>
      <title><![CDATA[Route360]]></title>
      <description><![CDATA[ Blog by a frontend developer ]]></description>
      <link>https://route360.dev/en/</link>
      <generator>GatsbyJS</generator>
      <lastBuildDate>Fri, 26 May 2023 03:04:23 GMT</lastBuildDate>
      <atom:link href="https://route360.dev/rss.en.xml" rel="self" type="application/rss+xml" />
      <item>
        <title>
        <![CDATA[ Generating a custom XML sitemap for a multilingual Gatsby site ]]>
        </title>
        <description>
        <![CDATA[ This blog (route360.dev) is a multilingual website generated by Gatsby.js. In this entry, I'll explain how to customize the xml sitemap witโ€ฆ ]]>
        </description>
        <link>https://route360.dev/en/post/gatsby-i18n-sitemap/</link>
        <guid isPermaLink="false">https://route360.dev/en/post/gatsby-i18n-sitemap/</guid>
        <pubDate>Fri, 26 May 2023 01:00:00 GMT</pubDate>
      </item>
      <item>
      <!-- snip -->
      </item>
   </channel>
</rss>

Explanation

The only thing you need to do is prepare the query for each language.

The code below is the English part;

{
  resolve: `gatsby-plugin-feed`,
  options: {
    query: `
      {
        site {
          siteMetadata {
            title
            siteUrl
          }
        }
      }
    `,
    feeds: [
      {
        // Filtering the latest 10 posts, English only
        query: `
          {
            allMarkdownRemark(
              filter: {
                fields: { language: { eq: "en" } }
              }
              sort: { frontmatter: { date: DESC } }
              limit: 10
            ) {
              nodes {
                excerpt
                frontmatter {
                  title
                  date
                }
                fields {
                  slug
                }
              }
            }
          }
        `,
        // Edit and generate data for RSS feeds
        serialize: ({ query: { site, allMarkdownRemark } }) => {
          return allMarkdownRemark.nodes.map(node => {
            return Object.assign({}, node.frontmatter, {
              description: node.excerpt,
              date: node.frontmatter.date,
              url: `${site.siteMetadata.siteUrl}/en/post/${node.fields.slug}/`,
              guid: `${site.siteMetadata.siteUrl}/en/post/${node.fields.slug}/`,
            })
          })
        },
        output: "/rss.en.xml", // Output filename
        title: "Route360",
        description: "Blog by a frontend developer",
        site_url: "https://route360.dev/en/",
        feed_url: "https://route360.dev/rss.en.xml",
      },
    ],
  },
},

Just repeat this for each language/locale. That's it!