Logo
Published on
·16 min read

Next.js - 블로그 다국어(i18n)로 만들기 Part 3

개요

Next.js - 블로그 다국어(i18n)로 만들기 Part 2 포스팅에서 눈에 보이는 부분은 정상 동작하는 것을 확인했습니다.

내용이 길어져 Part 1 ~ 4 로 나누어서 작성합니다.

Part 3에서는 rss, sitemap 을 수정합니다.


gitHub, i18n-tailwind-nextjs-starter-blog를 참고하여 만들었습니다.

Tailwind CSS Blog Starter 템플릿 사용을 결정하셨고, 프로젝트를 처음 시작한다면 위 프로젝트로 시작하는 것도 좋을 것 같습니다.

제 경우 이미 프로젝트가 진행 중이라서, 위 프로젝트를 참고하여 진행했습니다.

feed.xml

feed.xml을 생성하는 부분을 언어별로 생성하도록 수정합니다.

lib/generate-rss.js

  • locale 파라미터를 추가하고, languagedescription을 locale에 맞게 수정합니다.
// import 추가
import { formatSlug } from '@/lib/mdx'
import { i18n } from '../next-i18next.config'

// locale 파라미터 추가 및 guid, 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>
`

// locale 파라미터 추가, description, language 수정, generateRssItem 호출 시 locale 추가
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
  • 위 코드 중 siteMetadata.description[locale] 부분은 Part 4 siteMetadata 부분에서 설명합니다. 하지만 아래와 같이 siteMetadata.descriptionkoen을 추가하지 않으면 오류가 발생하므로 미리 추가합니다.
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

  • 언어별 feed를 생성하도록 아래와 같이 코드를 수정합니다.
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

  • 언어별 feed를 생성하도록 아래와 같이 코드를 수정합니다.
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

Tailwind CSS Blog Starter 템플릿에는 배포시 자동으로 sitemap을 생성하는 코드가 있습니다.

2개 이상의 언어가 있는 페이지의 경우 alternate link를 추가하도록 수정하겠습니다.

scripts/generate-sitemap.js

아래 코드와 주석을 참고하세요.

const fs = require('fs')
const globby = require('globby')
const matter = require('gray-matter')
const prettier = require('prettier')
const siteMetadata = require('../data/siteMetadata')
// 추가: i18n 설정을 불러옵니다.
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',
  ])

  // 추가: i18n 설정에서 locale 목록과 기본 locale을 불러옵니다.
  const { locales, defaultLocale } = i18n

  // 추가: routeMap = { path: ['en', ''] } 형태, default locale은 ''로 설정
  const routeMap = new Map()

  // 기존 내용에서 필터링 하는 부분은 그대로 유지하고, routeMap에 locale을 추가하는 부분을 추가
  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', '')
      // feed의 locale을 찾기 위해 replace 부분 수정
      .replace('/feed', '')
      .replace('.xml', '')
    const route = path === '/index' ? '' : path

    // /blog, /tags 하위에 있는 동적인 콘텐츠는 locale 확인 후 추가
    if (/^\/(blog|tags)\/[^/]+/.test(route)) {
      // .{locale} 포함여부 확인 위한 regex
      const regex = new RegExp(`\\.(${locales.join('|')})$`, 'i')
      const findLocale = route.match(regex)

      if (findLocale) {
        const locale = findLocale[1]
        const newRoute = route.replace(regex, '') // route에서 locale 제거
        routeMap.set(
          newRoute,
          routeMap.has(newRoute) ? [...routeMap.get(newRoute), locale] : [locale] // 해당 path에 포함된 locale을 배열에 추가
        )
      } else {
        routeMap.set(
          route,
          routeMap.has(route) ? [...routeMap.get(route), defaultLocale] : [defaultLocale]
        )
      }
    }
    // 동적 콘텐츠가 아닌 그 외의 페이지는 모든 locale 추가
    else {
      routeMap.set(
        route,
        locales.map((locale) => (locale === defaultLocale ? '' : locale))
      )
    }
  })

  // routeMap으로 url 목록 생성
  // 포함된 locale이 2개 이상이면 xhtml:link 추가, 아닐 경우는 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>`
    }
  })

  // 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)
})()

sitemap 확인

  • 명령 프롬프트에서 node ./scripts/generate-sitemap 명령어를 실행하면 public/sitemap.xml 파일이 생성됩니다.

아래와 같이 생성된 것을 확인할 수 있습니다.

<?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>
  <!-- 생략 -->
  <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>
  <!-- 생략 -->
</urlset>

정리

rss, sitemap을 다국어로 만드는 방법을 알아보았습니다.

Next.js - 블로그 다국어(i18n)로 만들기 Part 4 에서 html lang 설정 및 alternate link 태그를 추가하는 방법을 알아보겠습니다.

코드

변경된 코드는 commit history 를 참고해주세요.

모두의 구글 애널리틱스4:GA4로 하는 디지털 마케팅 데이터 분석, 길벗  아무나 쉽게 따라하는 블로그 마케팅:검색 상위노출을 위한 블로그 마케팅의 모든 것, 페이스메이커  트래픽을 쓸어 담는 검색엔진 최적화:검색엔진이 가장 좋아하는 사이트 만들기, e비즈북스  실전에서 바로 쓰는 Next.js:SSR부터 SEO 배포까지 확장성 높은 풀스택 서비스 구축 가이드, 한빛미디어
(위 링크는 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.)