- Published on
- ·15 min read
Next.js - Creating a Multilingual (i18n) Blog Part 4
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 3, we made modifications to RSS and Sitemap.
As the content has grown longer, we've split it into Parts 1 through 4.
In Part 4, we will add HTML lang settings and configure alternate settings for the added languages.
- Part 1: Installing next-i18next and Multilingual Site Setup
- Part 2: Multilingual Handling for Registered Posts
- Part 3: Handling Rss and Sitemap
- Part 4: Language, Alternate Tags, and Date Format Handling for SEO
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.
html lang
When you view the page source, you can see that it is fixed as follows regardless of the selected language.
<html lang="en" class="scroll-smooth">
Let's make this part automatically change depending on the selected language.
pages/_document.js
- When you open the file, you'll see that the code is fixed as
<Html lang="en">
. Remove thelang=".."
part.
class MyDocument extends Document {
render() {
return (
<Html className="scroll-smooth"> // Remove lang="en"
// ...
</Html>
)
}
}
export default MyDocument
After removing it, when you check the page source again, you will see that lang=".."
is automatically added according to the selected language.
This is possible for the following reasons.
Since Next.js knows what language the user is visiting it will automatically add the lang attribute to the <html> tag.
For more detailed information on this, you can refer to the Next.js Internationalization (i18n) Routing: Search Engine Optimization section.
siteMetadata
data/siteMetadata.js
- If you want to display the text specified in
siteMetadata
differently for each language, you can configure it by separating it by language as in thedescription
field below.
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.',
},
// ...
}
- Modify all places that use
siteMetadata.description
to acceptlocale
and display it accordingly. - Let's take the
pages/index.js
page code as an example.
export async function getStaticProps({ locale, defaultLocale, locales }) {
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales)
return { props: { posts, locale } } // Add locale
}
export default function Home({ posts, locale }) { // Add locale
return (
<>
<PageSEO title={siteMetadata.title} description={siteMetadata.description[locale]} /> // Add locale
<div className="...">
<div className="...">
<p className="...">
{siteMetadata.description[locale]} // Add locale
</p>
</div>
</div>
// ...
</>
)
}
alternate
When you have pages that use multiple languages, you can use alternate
to inform the user.
pages/_document.js
Remove the alternate code as follows. (Remove it here and move the placement to CommonSEO
component to add it according to the language.)
{/*<link rel="alternate" type="application/rss+xml" href="/feed.xml" />*/}
components/SEO.js
Modify the common SEO component, which is used universally, to receive the list of available languages as the
availableLocales
parameter.Modify
CommonSEO
const CommonSEO = ({
// ...
availableLocales, // Add availableLocales
}) => {
const router = useRouter()
return (
<Head>
// ...
// Add the alternate tags for the list of available languages.
{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>
)
}
- Modify
PageSEO
export const PageSEO = ({ title, description, availableLocales }) => { // Add availableLocales
// ...
return (
<CommonSEO
// ...
availableLocales={availableLocales} // Add availableLocales
/>
)
}
- Modify
TagSEO
export const TagSEO = ({ title, description, availableLocales }) => { // Add availableLocales
// ...
return (
<>
<CommonSEO
// ...
availableLocales={availableLocales} // Add availableLocales
/>
<Head>
<link
// ...
// Set the feed URL according to the current language.
href={`${siteMetadata.siteUrl}${router.asPath}/feed${
router.locale === router.defaultLocale ? '' : `.${router.locale}`
}.xml`}
/>
</Head>
</>
)
}
- Modify
BlogSEO
export const BlogSEO = ({
// ...
availableLocales, // Add availableLocales
}) => {
// ...
return (
<>
<CommonSEO
// ...
availableLocales={availableLocales} // Add availableLocales
/>
// ...
</>
)
}
PageSEO
Update Pages Using - Pass the
availableLocales
parameter to pages that use thePageSEO
component. - Taking
pages/blog.js
as an example, it would look like this.
// ...
export async function getStaticProps({ locale, defaultLocale, locales }) {
// ...
return {
props: {
// ...
availableLocales: locales, // Add availableLocales
},
}
}
export default function Blog({ posts, initialDisplayPosts, pagination, locale, availableLocales }) { // Add availableLocales
return (
<>
<PageSEO
// ...
availableLocales={availableLocales} // Add availableLocales
/>
// ...
</>
)
}
pages/tags/[tag].js
- For the tag pages, since they are dynamically generated, you need to find the list of available languages for each specific tag.
// ...
export async function getStaticProps({ params, locale, defaultLocale, locales }) {
// ...
// Find the list of available languages.
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 } } // Add availableLocales
}
export default function Tag({ posts, tag, availableLocales }) { // Add availableLocales
// ...
return (
<>
<TagSEO
// ...
availableLocales={availableLocales} // Add availableLocales
/>
<ListLayout posts={posts} title={title} />
</>
)
}
pages/blog/[...slug].js
- For blog posts, since they are dynamically generated pages, you need to find the list of available languages for each specific post.
You've added a function to filter the available languages as follows:
- Modify
lib/mdx.js
: Added thegetAvailableLocalesBySlug
function.
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)
}
- Modify
pages/blog/[...slug].js
import {
getAvailableLocalesBySlug, // Add getAvailableLocalesBySlug
// ...
} from '@/lib/mdx'
// ...
export async function getStaticProps({ params, locale, defaultLocale, locales }) {
// ...
// Call the function for filtering available languages.
const availableLocales = await getAvailableLocalesBySlug(
'blog',
params.slug.join('/'),
locale,
defaultLocale,
locales
)
// ...
return {
props: {
// ...
availableLocales, // Add availableLocales
},
}
}
export default function Blog({ post, authorDetails, prev, next, relatedPosts, availableLocales }) { // Add availableLocales
// ...
return (
<>
{frontMatter.draft !== true ? (
<MDXLayoutRenderer
// ...
availableLocales={availableLocales} // Add availableLocales
/>
) : (
<div className="mt-24 text-center">
// ...
</div>
)}
</>
)
}
- Modify the layout components used in
MDXLayoutRenderer
.- Taking
layouts/PostLayout.js
as an example, it would look like this.
- Taking
// ...
export default function PostLayout({
// ...
availableLocales, // Add availableLocales
}) {
// ...
return (
<SectionContainer>
<BlogSEO
// ...
availableLocales={availableLocales} // Add availableLocales
/>
// ...
</SectionContainer>
)
}
Change Date Format
Modify the code to use
locale
instead ofsiteMetadata.locale
when callingtoLocaleDateString
.Taking
lib/utils/formatDate.js
as an example, it would look like this.
// ...
const formatDate = (date, locale) => { // Add locale
const options = {
year: 'numeric',
month: 'long',
day: 'numeric',
}
const now = new Date(date).toLocaleDateString(locale ? locale : siteMetadata.locale, options) // Change locale
return now
}
export default formatDate
- Let's also provide an example for
layouts/PostLayout.js
.
// ...
import { useRouter } from 'next/router' // Add useRouter
// ...
export default function PostLayout({ // ...
}) {
const { slug, fileName, date, title, images, tags } = frontMatter
const { locale } = useRouter() // Add 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)} // Change locale
</time>
</dd>
</div>
</dl>
// ...
</div>
</header>
// ...
</div>
</article>
</SectionContainer>
)
}
Summary
Now, it seems like we've reached the end of our journey, at least for now.
Initially, I was diving into Next.js with the Tailwind CSS Blog Starter template without fully grasping how it worked. However, as I tackled multilingual support, I began to gain a better understanding of its inner workings.
Even if it's not the Tailwind CSS Blog Starter template, understanding how to filter and generate paths for multilingual support can be valuable knowledge that you can apply to other projects
Code
You can review the code on the multilingual-i18n-blog branch.
The GitHub source above may not be identical to the current blog, but it was created separately as a Git source to assist in understanding the content.