Logo
Published on
ยท16 min read

Next.js - Creating a Multilingual (i18n) Blog Part 3

I apologize in advance for any awkward expressions in English. ๐Ÿ™

English is not my native language, and I have relied on ChatGPT's assistance to proceed with the translation.

Overview

In Next.js - Creating a Multilingual (i18n) Blog Part 2, we confirmed that the visible parts are working correctly.

As the content has grown longer, we've split it into Parts 1 through 4.

In Part 3, we will make modifications to the RSS and Sitemap.


I created this project with reference to gitHub, i18n-tailwind-nextjs-starter-blog.

If you've decided to use the Tailwind CSS Blog Starter template and are starting your project from scratch, it might be a good idea to start with the above project.

In my case, since the project was already in progress, I proceeded with it by referring to the mentioned project.

feed.xml

Modify the part that generates feed.xml to create it in each language.

lib/generate-rss.js

  • Add the locale parameter and update language and description to match the locale.
// Add import
import { formatSlug } from '@/lib/mdx'
import { i18n } from '../next-i18next.config'

// Add the locale parameter and update guid and link.
const generateRssItem = (post, locale = 'ko') => `
  <item>
    <guid>${siteMetadata.siteUrl}${locale === i18n.defaultLocale?'': '/' + locale}/blog/${formatSlug(post.slug, i18n.locales)}</guid>
    <title>${escape(post.title)}</title>
    <link>${siteMetadata.siteUrl}${locale === i18n.defaultLocale?'': '/' + locale}/blog/${formatSlug(post.slug, i18n.locales)}</link>
    ${post.summary && `<description>${escape(post.summary)}</description>`}
    <pubDate>${new Date(post.date).toUTCString()}</pubDate>
    <author>${siteMetadata.email} (${siteMetadata.author})</author>
    ${post.tags && post.tags.map((t) => `<category>${t}</category>`).join('')}
  </item>
`

// Add the locale parameter, update description and language, and include the locale when calling generateRssItem.
const generateRss = (posts, page = 'feed.xml', locale = 'ko') => `
  <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
      <title>${escape(siteMetadata.title)}</title>
      <link>${siteMetadata.siteUrl}/blog</link>
      <description>${escape(siteMetadata.description[locale])}</description>
      <language>${locale}</language>
      <managingEditor>${siteMetadata.email} (${siteMetadata.author})</managingEditor>
      <webMaster>${siteMetadata.email} (${siteMetadata.author})</webMaster>
      <lastBuildDate>${new Date(posts[0].date).toUTCString()}</lastBuildDate>
      <atom:link href="${siteMetadata.siteUrl}/${page}" rel="self" type="application/rss+xml"/>
      ${posts.map((post) => generateRssItem(post, locale)).join('')}
    </channel>
  </rss>
`
export default generateRss
  • In the code snippet you provided, the siteMetadata.description[locale] part is explained in Part 4 under the siteMetadata section. However, to prevent any errors, you should add ko and en to siteMetadata.description as follows:
const siteMetadata = {
  // ...
  description: {
    ko: '์ƒํ™œ ์ •๋ณด, ๊ฐœ๋ฐœ ์ •๋ณด ๋“ฑ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋‹ค๋ฃจ๋Š” ๋ธ”๋กœ๊ทธ์ž…๋‹ˆ๋‹ค.',
    en: 'This is a blog that covers various interests such as lifestyle information, development updates, and more.',
  },
  // ...
}

pages/blog/[...slug].js

  • Modify the code as follows to create feeds for each language.
export async function getStaticProps({ params, locale, defaultLocale, locales }) {
  // ...

  // rss
  if (allPosts.length > 0) {
    const feedName = locale === defaultLocale ? 'feed.xml' : `feed.${locale}.xml`
    const rss = generateRss(allPosts, feedName, locale)
    fs.writeFileSync(`./public/${feedName}`, rss)
  }

  return { props: { post, authorDetails, prev, next, relatedPosts } }
}

pages/tags/[tag].js

  • Modify the code as follows to create feeds for each language.
export async function getStaticProps({ params, locale, defaultLocale, locales }) {
  // ...

  // rss
  if (filteredPosts.length > 0) {
    const feedName = locale === defaultLocale ? 'feed.xml' : `feed.${locale}.xml`
    const rss = generateRss(filteredPosts, `tags/${params.tag}/${feedName}`, locale)
    const rssPath = path.join(root, 'public', 'tags', params.tag)
    fs.mkdirSync(rssPath, { recursive: true })
    fs.writeFileSync(path.join(rssPath, feedName), rss)
  }

  return { props: { posts: filteredPosts, tag: params.tag } }
}

sitemap

The Tailwind CSS Blog Starter template has code that automatically generates a sitemap during deployment.

For pages with more than two languages, we will modify it to include alternate links.

scripts/generate-sitemap.js

Please refer to the code and comments below:

const fs = require('fs')
const globby = require('globby')
const matter = require('gray-matter')
const prettier = require('prettier')
const siteMetadata = require('../data/siteMetadata')
// Added: Importing the i18n configuration.
const { i18n } = require('../next-i18next.config')

;(async () => {
  const prettierConfig = await prettier.resolveConfig('./.prettierrc.js')
  const pages = await globby([
    'pages/*.js',
    'pages/*.tsx',
    'data/blog/**/*.mdx',
    'data/blog/**/*.md',
    'public/tags/**/*.xml',
    '!pages/_*.js',
    '!pages/_*.tsx',
    '!pages/api',
  ])

  // Added: Loading the list of locales and the default locale from the i18n configuration.
  const { locales, defaultLocale } = i18n

  // Added: routeMap = { path: ['en', ''] } format with the default locale set to ''.
  const routeMap = new Map()

  // Keep the existing part for filtering as it is, and add the part for adding locales to routeMap.
  pages.forEach((page) => {
    // Exclude drafts from the sitemap
    if (page.search('.md') >= 1 && fs.existsSync(page)) {
      const source = fs.readFileSync(page, 'utf8')
      const fm = matter(source)
      if (fm.data.draft) {
        return
      }
      if (fm.data.canonicalUrl) {
        return
      }
    }

    if (page.search('pages/404.') > -1 || page.search(`pages/blog/[...slug].`) > -1) {
      return
    }

    const path = page
      .replace('pages/', '/')
      .replace('data/blog', '/blog')
      .replace('public/', '/')
      .replace('.js', '')
      .replace('.tsx', '')
      .replace('.mdx', '')
      .replace('.md', '')
      // Modify the replace part to find the locale for the feed.
      .replace('/feed', '')
      .replace('.xml', '')
    const route = path === '/index' ? '' : path

    // Dynamic content under /blog and /tags should be added after checking the locale.
    if (/^\/(blog|tags)\/[^/]+/.test(route)) {
      // Regular expression to check for the presence of .{locale}.
      const regex = new RegExp(`\\.(${locales.join('|')})$`, 'i')
      const findLocale = route.match(regex)

      if (findLocale) {
        const locale = findLocale[1]
        const newRoute = route.replace(regex, '') // Remove the locale from the route
        routeMap.set(
          newRoute,
          routeMap.has(newRoute) ? [...routeMap.get(newRoute), locale] : [locale] // Add the locale included in the path to an array
        )
      } else {
        routeMap.set(
          route,
          routeMap.has(route) ? [...routeMap.get(route), defaultLocale] : [defaultLocale]
        )
      }
    }
    // For non-dynamic content pages, add all locales.
    else {
      routeMap.set(
        route,
        locales.map((locale) => (locale === defaultLocale ? '' : locale))
      )
    }
  })

  // Generate a list of URLs using routeMap.
  // If there are more than two included locales, add xhtml:link; otherwise, add only the URL.
  let sitemapUrls = ''
  routeMap.forEach((value, key) => {
    if (value.length > 1) {
      sitemapUrls += `
    <url>
        <loc>${siteMetadata.siteUrl}${key}</loc>
        ${value
          .map((locale) => {
            return `<xhtml:link rel="alternate" hreflang="${
              locale === '' ? defaultLocale : locale
            }" href="${siteMetadata.siteUrl}${locale === '' ? '' : '/' + locale}${key}" />`
          })
          .join('')}
    </url>`
    } else {
      sitemapUrls += `
    <url>
        <loc>${siteMetadata.siteUrl}${key}</loc>
    </url>`
    }
  })

  // Generate the sitemap
  const sitemap = `
  <?xml version="1.0" encoding="UTF-8"?>
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml">
    ${sitemapUrls}
  </urlset>`

  const formatted = prettier.format(sitemap, {
    ...prettierConfig,
    parser: 'html',
  })

  // eslint-disable-next-line no-sync
  fs.writeFileSync('public/sitemap.xml', formatted)
})()

Checking the Sitemap

  • When you run the node ./scripts/generate-sitemap command in the command prompt, the public/sitemap.xml file will be generated.

You can confirm that it has been generated as follows.

<?xml version="1.0" encoding="UTF-8"?>
<urlset
  xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
  xmlns:xhtml="http://www.w3.org/1999/xhtml"
>
  <url>
    <loc>https://hlog.kr/about</loc>
    <xhtml:link rel="alternate" hreflang="ko" href="https://hlog.kr/about" />
    <xhtml:link rel="alternate" hreflang="en" href="https://hlog.kr/en/about" />
  </url>
  <url>
    <loc>https://hlog.kr/blog</loc>
    <xhtml:link rel="alternate" hreflang="ko" href="https://hlog.kr/blog" />
    <xhtml:link rel="alternate" hreflang="en" href="https://hlog.kr/en/blog" />
  </url>
  <!-- omitted -->
  <url>
    <loc>https://hlog.kr/blog/2023-09/javascript-date-format-function</loc>
  </url>
  <url>
    <loc>https://hlog.kr/blog/2023-09/javascript-zero-fill-three-ways</loc>
  </url>
  <url>
    <loc>https://hlog.kr/blog/2023-09/nextjs-blog-multi-language-part1</loc>
    <xhtml:link
      rel="alternate"
      hreflang="en"
      href="https://hlog.kr/en/blog/2023-09/nextjs-blog-multi-language-part1"
    />
    <xhtml:link
      rel="alternate"
      hreflang="ko"
      href="https://hlog.kr/ko/blog/2023-09/nextjs-blog-multi-language-part1"
    />
  </url>
  <url>
    <loc>https://hlog.kr/blog/2023-09/nextjs-blog-multi-language-part2</loc>
    <xhtml:link
      rel="alternate"
      hreflang="en"
      href="https://hlog.kr/en/blog/2023-09/nextjs-blog-multi-language-part2"
    />
    <xhtml:link
      rel="alternate"
      hreflang="ko"
      href="https://hlog.kr/ko/blog/2023-09/nextjs-blog-multi-language-part2"
    />
  </url>
  <url>
    <loc>https://hlog.kr/tags/nextjs</loc>
    <xhtml:link rel="alternate" hreflang="en" href="https://hlog.kr/en/tags/nextjs" />
    <xhtml:link rel="alternate" hreflang="ko" href="https://hlog.kr/ko/tags/nextjs" />
  </url>
  <url>
    <loc>https://hlog.kr/tags/tip</loc>
  </url>
  <!-- omitted -->
</urlset>

Summary

We've learned how to make RSS and sitemap multilingual.

In Next.js - Creating a Multilingual (i18n) Blog Part 4, we will explore how to set the HTML lang attribute and add alternate link tags.

Code

The updated code can be found in the commit history.