- Published on
- ·14 min read
Next.js - 블로그 다국어(i18n)로 만들기 Part 4
개요
Next.js - 블로그 다국어(i18n)로 만들기 Part 3 포스팅에서 rss, sitemap 을 수정했습니다.
내용이 길어져 Part 1 ~ 4 로 나누어서 작성합니다.
Part 4에서는 html lang 설정과, 추가된 언어에 대한 alternate 설정을 추가합니다.
- Part 1: next-i18next 설치와 사이트 다국어 처리
- Part 2: 등록된 글들에 대한 다국어 처리
- Part 3: Rss와 Sitemap 처리
- Part 4: SEO를 위한 lang, alternate 그리고 날짜 형식 처리
gitHub, i18n-tailwind-nextjs-starter-blog를 참고하여 만들었습니다.
Tailwind CSS Blog Starter 템플릿 사용을 결정하셨고, 프로젝트를 처음 시작한다면 위 프로젝트로 시작하는 것도 좋을 것 같습니다.
제 경우 이미 프로젝트가 진행 중이라서, 위 프로젝트를 참고하여 진행했습니다.
html lang
페이지를 소스보기로 확인해보면 설정된 언어와 상관없이 아래와 같이 고정되어있습니다.
<html lang="en" class="scroll-smooth">
이 부분을 언어에 따라 자동으로 변경되도록 하겠습니다.
pages/_document.js
- 파일을 열어보면
<Html lang="en">
로 코드가 고정되어있습니다.lang=".."
부분을 제거합니다.
class MyDocument extends Document {
render() {
return (
<Html className="scroll-smooth"> // lang="en" 삭제
// ...
</Html>
)
}
}
export default MyDocument
제거 후 다시 소스보기로 확인해보면 변경된 언어에 맞게 lang=".."
가 자동으로 추가되는 것을 확인할 수 있습니다.
이것이 가능한 이유는 아래와 같습니다.
Since Next.js knows what language the user is visiting it will automatically add the lang attribute to the <html> tag.
해석: Next.js는 사용자가 어떤 언어로 방문했는지 알고 있으므로 자동으로 <html> 태그에 lang 속성을 추가합니다.
이에 대한 자세한 내용은 Next.js- Internationalization (i18n) Routing: Search Engine Optimization 부분을 참고하시면 됩니다.
siteMetadata
data/siteMetadata.js
siteMetadata
에 설정된 text를 언어별로 다르게 표시할 경우 아래 코드의description
항목처럼 언어별로 구분하여 설정합니다.
const siteMetadata = {
title: 'HLog',
author: 'hlog',
headerTitle: 'HLog',
description: {
ko: '생활 정보, 개발 정보 등 여러가지 관심사를 다루는 블로그입니다.',
en: 'This is a blog that covers various interests such as lifestyle information, development updates, and more.',
},
// ...
}
siteMetadata.description
항목을 사용하는 모든 곳에locale
을 전달받아 표시하도록 수정합니다.pages/index.js
페이지 코드를 예로 들어보겠습니다.
export async function getStaticProps({ locale, defaultLocale, locales }) {
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales)
return { props: { posts, locale } } // locale 추가
}
export default function Home({ posts, locale }) { // locale 추가
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description[locale]} /> // locale 추가
<div className="...">
<div className="...">
<p className="...">
{siteMetadata.description[locale]} // locale 추가
</p>
</div>
</div>
// ...
</>
)
}
alternate
여러 언어를 사용하는 페이지가 있을 경우 alternate
를 사용하여 사용자에게 알려줄 수 있습니다.
pages/_document.js
아래와 같이 alternate 코드를 제거합니다. (여기서 제거하고 CommonSEO
컴포넌트에서 언어에 맞게 추가되도록 위치를 변경합니다.)
{/*<link rel="alternate" type="application/rss+xml" href="/feed.xml" />*/}
components/SEO.js
사용 가능한 언어 목록을
availableLocales
파라미터로 받도록 공통으로 사용하는 SEO 컴포넌트를 수정합니다.CommonSEO
수정
const CommonSEO = ({
// ...
availableLocales, // availableLocales 추가
}) => {
const router = useRouter()
return (
<Head>
// ...
// 사용 가능한 언어 목록에 대한 `alternate` 태그를 추가
{availableLocales &&
availableLocales.length > 1 &&
availableLocales.map((locale) => (
<link
rel="alternate"
hrefLang={locale}
href={`${siteMetadata.siteUrl}${locale === router.defaultLocale ? '' : `/${locale}`}${
router.asPath
}`}
key={locale}
/>
))}
<link
rel="alternate"
type="application/rss+xml"
href={`/feed${router.locale === router.defaultLocale ? '' : `.${router.locale}`}.xml`}
/>
</Head>
)
}
PageSEO
수정
export const PageSEO = ({ title, description, availableLocales }) => { // availableLocales 추가
// ...
return (
<CommonSEO
// ...
availableLocales={availableLocales} // availableLocales 추가
/>
)
}
TagSEO
수정
export const TagSEO = ({ title, description, availableLocales }) => { // availableLocales 추가
// ...
return (
<>
<CommonSEO
// ...
availableLocales={availableLocales} // availableLocales 추가
/>
<Head>
<link
// ...
// 현재 언어에 맞는 feed 주소를 설정
href={`${siteMetadata.siteUrl}${router.asPath}/feed${
router.locale === router.defaultLocale ? '' : `.${router.locale}`
}.xml`}
/>
</Head>
</>
)
}
BlogSEO
수정
export const BlogSEO = ({
// ...
availableLocales, // availableLocales 추가
}) => {
// ...
return (
<>
<CommonSEO
// ...
availableLocales={availableLocales} // availableLocales 추가
/>
// ...
</>
)
}
PageSEO
사용 페이지들 수정
PageSEO
컴포넌트를 사용하는 페이지들에availableLocales
파라미터를 전달합니다.pages/blog.js
를 예로 들면 아래와 같습니다.
// ...
export async function getStaticProps({ locale, defaultLocale, locales }) {
// ...
return {
props: {
// ...
availableLocales: locales, // availableLocales 추가
},
}
}
export default function Blog({ posts, initialDisplayPosts, pagination, locale, availableLocales }) { // availableLocales 추가
return (
<>
<PageSEO
// ...
availableLocales={availableLocales} // availableLocales 추가
/>
// ...
</>
)
}
pages/tags/[tag].js
- tag의 경우 동적으로 생성되는 페이지이므로, 해당 태그에 대해 사용 가능한 언어 목록을 찾아야합니다.
// ...
export async function getStaticProps({ params, locale, defaultLocale, locales }) {
// ...
// 사용 가능한 언어 목록을 알아냅니다.
const availableLocales = []
await Promise.all(
locales.map(async (local) => {
const tags = await getAllTags('blog', local, defaultLocale, locales)
if (tags[params.tag] !== undefined) availableLocales.push(local)
})
)
// ...
return { props: { posts: filteredPosts, tag: params.tag, availableLocales } } // availableLocales 추가
}
export default function Tag({ posts, tag, availableLocales }) { // availableLocales 추가
// ...
return (
<>
<TagSEO
// ...
availableLocales={availableLocales} // availableLocales 추가
/>
<ListLayout posts={posts} title={title} />
</>
)
}
pages/blog/[...slug].js
- 블로그 글의 경우 동적으로 생성되는 페이지이므로 해당 글에 대한 사용 가능한 언어 목록을 찾아야합니다.
사용 가능한 언어를 필터링하는 함수를 아래와 같이 추가했습니다.
lib/mdx.js
수정 :getAvailableLocalesBySlug
함수 추가
export async function getAvailableLocalesBySlug(
type,
slug,
locale = null,
defaultLocale = null,
locales = null
) {
return locales
.map((locale) => {
const localeExtention = locale === defaultLocale ? '' : `.${locale}`
const mdxPath = path.join(root, 'data', type, `${slug}${localeExtention}.mdx`)
const mdPath = path.join(root, 'data', type, `${slug}${localeExtention}.md`)
if (fs.existsSync(mdxPath) || fs.existsSync(mdPath)) {
return locale
}
})
.filter((locale) => locale !== undefined)
}
pages/blog/[...slug].js
수정
import {
getAvailableLocalesBySlug, // getAvailableLocalesBySlug 추가
// ...
} from '@/lib/mdx'
// ...
export async function getStaticProps({ params, locale, defaultLocale, locales }) {
// ...
// 사용 가능한 언어 필터링 함수 호출
const availableLocales = await getAvailableLocalesBySlug(
'blog',
params.slug.join('/'),
locale,
defaultLocale,
locales
)
// ...
return {
props: {
// ...
availableLocales, // availableLocales 추가
},
}
}
export default function Blog({ post, authorDetails, prev, next, relatedPosts, availableLocales }) { // availableLocales 추가
// ...
return (
<>
{frontMatter.draft !== true ? (
<MDXLayoutRenderer
// ...
availableLocales={availableLocales} // availableLocales 추가
/>
) : (
<div className="mt-24 text-center">
// ...
</div>
)}
</>
)
}
MDXLayoutRenderer
에서 사용하는 layout 컴포넌트들을 수정합니다.layouts/PostLayout.js
를 예로 들면 아래와 같습니다.
// ...
export default function PostLayout({
// ...
availableLocales, // availableLocales 추가
}) {
// ...
return (
<SectionContainer>
<BlogSEO
// ...
availableLocales={availableLocales} // availableLocales 추가
/>
// ...
</SectionContainer>
)
}
날짜 형식 변경
toLocaleDateString
호출 시siteMetadata.locale
대신locale
을 사용하도록 수정합니다.lib/utils/formatDate.js
를 예로 들겠습니다.
// ...
const formatDate = (date, locale) => { // locale 추가
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
}
const now = new Date(date).toLocaleDateString(locale ? locale : siteMetadata.locale, options) // locale로 변경
return now
}
export default formatDate
layouts/PostLayout.js
도 추가로 예로 들겠습니다.
// ...
import { useRouter } from 'next/router' // useRouter 추가
// ...
export default function PostLayout({ // ...
}) {
const { slug, fileName, date, title, images, tags } = frontMatter
const { locale } = useRouter() // locale 추가
return (
<SectionContainer>
// ...
<ScrollTopAndComment />
<article>
<div className="...">
<header className="...">
<div className="...">
<dl className="...">
<div>
<dt className="...">Published on</dt>
<dd className="...">
<time dateTime={date}>
{new Date(date).toLocaleDateString(locale, postDateTemplate)} // locale 로 변경
</time>
</dd>
</div>
</dl>
// ...
</div>
</header>
// ...
</div>
</article>
</SectionContainer>
)
}
정리
이제 모든(?) 작업이 끝났습니다.
Next.js를 Tailwind CSS Blog Starter 템플릿으로 처음 접하고 어떻게 동작하는 지도 모르고 쓰고 있었는데 다국어 처리를 하며 어떻게 동작하고 있었는 지 조금은 파악할 수 있게 되었습니다.
꼭 Tailwind CSS Blog Starter 템플릿이 아니더라도 다국어 처리를 위해 어떤 방식으로 필터링하고, path를 생성하는 지 파악하면 다른 프로젝트에도 적용할 수 있을 것 같습니다.
코드
코드는 multilingual-i18n-blog 브랜치에서 확인하실 수 있습니다.
위 GitHub 소스는 현재 블로그와 완전히 동일하지는 않지만, 글에 대한 이해를 돕기 위해 따로 작성한 Git 소스로 참고하시면 될 것 같습니다.