A multilingual blog in NextJS. The basics.
Aug 28, 2021
ℹ️ Update - 25/01/2022: While the approach here works just fine and is up to date, I found the value it brings is little compared to the cost of implementing this. If your audience is really in several languages, there could be easier ways to tackle the problem.
How such a simple solution, escaped me for quite some time is something that I am still figuring out. What I know almost for certain is there are not many people with their blog in several languages, using internazionalization, or more rarely commonly known as multilingual blog.
This was more a challenge, rather than a real requirement for me, to the date I have little to no content to share, why would I even need another language? Well it all started with the following idea: Let’s use next, and a couple of Markdown files to put together a website for my wedding 🤔. As it turns out, I didn’t find this so simple back then, so I went ahead and built the whole web app as I already knew how to.
Once that was done, and I had some time to focus on solving the original problem, here is what my requirements looked like and how I did it.
What do we want to achieve?
Here is the folder structure we will be looking at, paying special attention to those pointed out 👈
$ miguel > tree -L 3 .
├── data
│ └── blog
│ ├── en
│ │ ├── my-first-post.mdx
│ │ └── test-post.mdx
│ └── es
│ └── test-post.mdx
└── src ├── components
│ ├── ...
│ └── ...
├── layouts
│ └── blog.tsx 👈
├── lib
│ └── mdx.ts 👈
└── pages
├── _app.tsx
├── _document.tsx
├── blog
│ └── [slug].tsx 👈
├── blog.tsx 👈
└── index.tsx
To summarize
- /src/layouts/blog.tsx contains the layout for the blog components
- /src/lib/mdx.ts contains helper functions that will serve us to retrieve files when the code executes on the server
- /src/pages/blog.tsx contains the actual page that will list the blog entries
- /src/pages/blog/[slug].tsx will be the actual blog post page using dynamic routing.
Let’s start with the basics
In NextJS you can hve dynamic routing, which enables you to have a file named as [filename].txs that will be used as a placeholder for the different slugs. Slugs, in our case will be the file names of our posts written in MDX files.
Get static paths for current locale
We need getStaticPaths to return all potentiall paths for our blog (read more in the official documentation). The important piece, thinking in translation is:
- We need to return the paths for which there is an article in the current locale.
- In getStaticProps, when calling getFileBySlug we should return the file for the current locale.
export function getFiles(type, locale) {
return fs.readdirSync(path.join(root, "data", type, locale));
}
export async function getFileBySlug(type, slug, locale) {
const source = slug
? fs.readFileSync(path.join(root, "data", type, locale, `${slug}.mdx`), "utf8")
: fs.readFileSync(path.join(root, "data", `${type}.mdx`), "utf8");
const { data, content } = matter(source);
const mdxSource = await serialize(content, {
mdxOptions: {
remarkPlugins:
[
require("remark-autolink-headings"),
require("remark-slug")
]
},
});
return {
mdxSource,
frontMatter: {
wordCount: content.split(/+/gu).length,
readingTime: readingTime(content),
slug: slug || null,
...data,
},
};
}
export default function Blog(props) {
const { mdxSource, frontMatter } = props.post;
return <MDXRemote {...mdxSource} components={MDXComponents} />;
}
export async function getStaticProps(props) {
const { params, locale } = props;
const post = await getFileBySlug("blog", params.slug, locale);
return { props: { post, locale } };
}
export async function getStaticPaths({ locales }) {
let paths = [];
locales.forEach((locale) => {
getFiles("blog", locale).forEach((path) => {
paths.push({ params: { slug: path.replace(/.mdx/, "") }, locale });
});
});
return { paths, fallback: false };
}
Blog page: listing by locale
As mentioned, the file src/pages/blog.tsx contains the blog entries. Our main goal here is to display the posts by locale - we will do so by including that information in the frontmatter piece as locale: en below is an example of this same file.
---
title: "My multilingual blog"
publishedAt: "2020-03-22"
summary: "How to implement a multilingual blog with "locale routing" following the folder structure"
locale: en
tags:
- blog
- nextjs
---
How such a simple solution,...
export async function getStaticProps({ locale }) {
const posts = await getAllFilesFrontMatter("blog", locale);
return { props: { posts } };
}
export async function getAllFilesFrontMatter(type, locale) {
const files = getFiles(type, locale);
return files
.map((blog) => {
const slug = blog.replace(/.mdx/, "");
const source = fs.readFileSync(path.join(root, "data", type, locale, `${slug}.mdx`), "utf8");
const { data, content } = matter(source.trim());
return {
...(data as Frontmatter),
slug,
readingTime: readingTime(content),
};
})
.filter((blog) => blog.locale === locale);
}
Once we have the files frontmatter, we can display only those for the current locale, as well as showing other information of the actual posts like publication date.
Recap
At this point, we should be able to
- show the posts for the current locale.
- click and navigate through locales seamlessly when on a post (👀 if the post has an alternate version in target locale)
Useful resources
general
- MDX Remote Example
- Blog Starter - A statically generated blog example using Next.js and Markdown
- Markdown/MDX with NextJS
linking locale to getStaticPaths
- How to return multiple locales dynamically
- How to setup getStaticPaths of multi-locale dynamic pages in Next.js [answer]