Aug 28, 2021
A multilingual blog in NextJS. The basics.
βΉοΈΒ 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?
-
Blog page that lists entries by language
-
Route that shows blog entries by slug reading from local
.mdfiles -
Language switch that redirects automatically to the alternate route.
- This must be achieved without manual intervention. If there is a file, with the same slug in a diferent locale location. That one must be served.
-
If article does not exist in that language
- show 404 Not Found
- and display available language when trying to access the slug.
-
(optional) List available languages for clicking on the side page
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.tsxTo summarize
/src/layouts/blog.tsxcontains the layout for the blog components/src/lib/mdx.tscontains helper functions that will serve us to retrieve files when the code executes on the server/src/pages/blog.tsxcontains the actual page that will list the blog entries/src/pages/blog/[slug].tsxwill 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 callinggetFileBySlugwe 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
-
Blog Starter - A statically generated blog example using Next.js and Markdown
-
Markdown/MDX with NextJS **linking locale to **
getStaticPaths -
How to setup getStaticPaths of multi-locale dynamic pages in Next.js [answer]