Before we start let’s state the things we want to achieve:
- We want to create a blog that handles content through Markdown files.
- We need to be able to access the web from different languages, that is, a different URL for each language.
- We also want that when we change the language on the article page, it will change language.
- We want to be able to detect the language of our browser so that the change is automatic, but we do not want to force it to always happen, because we want to allow anyone to see the content in the language they want regardless of the language of their browser.
- We also want it to be easily scalable, so we don’t have to work on this again if someday in the future we want to add another language.
Create a blog in Gatbsy
To create a blog in Gatsby based on Markdown files we can simply use a starter like gatsby-starter-blog. But in my case I’m going to start from scratch, but if you already have your blog ready you can skip this step.
In case you want to create it from scratch with me you can follow the Gatsby documentation to get started quickly, or what I like the most, install the Gatsby CLI and run gatsby new
in console.
When running gatsby new
we will be asked for the name of our project, the name of the folder where it will be saved, I called it “Multilingual Blog”.
Once chosen it will ask us if we are going to use any CMS, this is not our case so we select “No”. It will also ask us how we are going to style our components, something that we are not going to touch in this article.
Finally it asks us if we want to install additional plugins. For our blog we will need Markdown support. The rest of the plugins are at your discretion.
Once the configuration is finished we will have a project with a folder structure like this:
multilingual-blog/
├── public/
├── node_modules/
├── src/
│ ├── images/
│ └── pages/
│ ├── 404.js
│ └── index.js
└── gatsby-config.js
We are going to need a couple of extra files that don’t come with creating the site from the Gatsby CLI.
- The first one is to add
gatsby-node.js
to the root of the project. From that file we will be able to configure the URLs and the content of the dynamic pages of our site, among other things. - We are going to add the
blog
folder to the root of the project. In it we are going to create a subfolder for each article that we write, and inside these we will put theindex.lang.md
files. Of course.lang
is going to be replaced by the language we are going to use, for this example it will been
(English) andes
(Spanish). - Let’s go ahead and create the
locales
folder at the root of the project. This folder will contain subfolders for each language we are going to use in our blog (locales/en/, locales/en/, etc). Inside each subfolder will be acommon.json
file, which is the file where we are going to save the sentences we need to translate. - We are going to create the
templates
folder inside thesrc
folder. This folder is going to contain the files of the pages that we are going to create programmatically, in our case we could already create thearticle.js
file. - We are going to add the
languages.js
file to the root. In that file we are going to export an object with the languages we are going to use, so in case we want to scale our project to more languages we can change it directly from there. - In an extra step I will change the extension of the
.js
files to.tsx
, to be able to use TypeScript, the superset of JavaScript. **The code I publish from now on will always be in TypeScript.
Doing all this we would end up with a structure similar to this:
multilingual-blog/
├── public/
├── node_modules/
├── blog/│ └── my-first-article/ << El nombre será la URL del artículo│ ├── index.en.md│ └── index.es.md├── locales/│ └── en/│ │ └── common.json│ └── es/│ └── 404.json├── src/
│ ├── images/
│ ├── pages/
│ │ ├── 404.tsx
│ │ └── index.tsx
│ └── templates/│ └── article.tsx├── gatsby-config.js
├── gatsby-node.js├── languages.js└── tsconfig.json
Configure the tsconfig.json
file
To use TypeScript in Gatsby it is recommended to create a tsconfig.json
file in the root of the file.
I leave the content of my file, but for more details there is always the official documentation.
I have to mention that the baseUrl
property will change the way I import some files, from using a relative path to an absolute one. In Visual Studio Code this can also be done with JavaScript by creating a similar file called jsconfig.json
.
To make this change effective you have to add a block of code to gatsby-node.js
, but that is something I will do later.
{
"compilerOptions": {
"baseUrl": "src", "target": "es6",
"module": "commonjs",
"jsx": "react",
"strict": true,
"noImplicitAny": false,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
Setting up the languages.js
file
We have just created a file that is going to export an object with the languages that we are going to use in our project. In our case we are going to use English and Spanish, but we want our default page to be in English, so the file we have to create would be like this:
module.exports = {
__DEFAULT_LANGUAGE__: "en",
__LANGUAGES__: ["en", "es"],
};
Add i18next
to our project
To add i18next we will use the plugin gatsby-plugin-react-i18next, we can do it by running the following command:
npm install --save gatsby-plugin-react-i18next i18next react-i18next
Or using yarn
yarn add gatsby-plugin-react-i18next i18next react-i18next
Configure gatsby.config.js
Having done all of the above our gatsby-config.js
file will look something like this:
module.exports = {
siteMetadata: {
title: `multilingual-blog`,
siteUrl: `https://www.yourdomain.tld`,
},
plugins: [
"gatsby-plugin-react-helmet",
"gatsby-transformer-remark",
{
resolve: "gatsby-source-filesystem",
options: {
name: "pages",
path: "./src/pages/",
},
__key: "pages",
},
],
};
Before starting with the configuration you can remove the code corresponding to gatsby-source-filesystem
, since we will add ours next.
The variables inside siteMetadata
can be modified as you wish.
Import the object with the languages of our languages.js
.
To do this we simply paste this line at the top.
We will use these variables later when we configure i18next.
const { __DEFAULT_LANGUAGE__, __LANGUAGES__ } = require("./languages");
Add the blog
folder and locales
folder to gatsby-source-filesystem
.
It is important to add the blog
folder so that Gatsby knows that this is where it has to get the .md
files from, as well as the locales
folder, so that it knows where it has to get the .json
files with our translations.
We do this by adding this code block to the plugin array.
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/blog`,
name: `articles`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/locales`,
name: `locale`, // This name is important because we will use it later. },
},
Configure gatsby-plugin-react-i18next
.
Now let’s add and configure the gatsby-plugin-react-i18next
plugin.
You can read everything in more detail in their documentation, but in the end you will end up with code similar to this inside the plugin array.
{
resolve: `gatsby-plugin-react-i18next`,
options: {
localeJsonSourceName: `locale`, // It must be the same name we used in the previous configuration. defaultLanguage: __DEFAULT_LANGUAGE__,
languages: __LANGUAGES__,
redirect: true,
generateDefaultLanguagePage: true,
siteUrl: `https://example.com`,
i18nextOptions: {
interpolation: {
escapeValue: false,
},
keySeparator: false,
nsSeparator: false,
},
pages: [
{
matchPath: '/:lang/blog/:slug*', // Our blog address getLanguageFromPath: true,
},
],
},
},
The result of our configuration
With this we finished configuring gatsby-config.js
, the complete file would look like this:
const { __DEFAULT_LANGUAGE__, __LANGUAGES__ } = require("./languages");
module.exports = {
siteMetadata: {
title: `multilingual-blog`,
siteUrl: `https://www.example.com`,
},
plugins: [
"gatsby-plugin-react-helmet",
"gatsby-transformer-remark",
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/blog`,
name: `articles`,
},
},
{
resolve: `gatsby-source-filesystem`,
options: {
path: `${__dirname}/locales`,
name: `locale`, // This name is important because we will use it later.
},
},
{
resolve: `gatsby-plugin-react-i18next`,
options: {
localeJsonSourceName: `locale`, // It must be the same name we used in the previous configuration.
defaultLanguage: __DEFAULT_LANGUAGE__,
languages: __LANGUAGES__,
redirect: true,
generateDefaultLanguagePage: true,
siteUrl: `https://example.com/`,
i18nextOptions: {
interpolation: {
escapeValue: false,
},
keySeparator: false,
nsSeparator: false,
},
pages: [
{
matchPath: '/:lang/blog/:slug*', // Our blog address
getLanguageFromPath: true,
},
],
},
},
],
};
Create some placeholders articles
In order to test our blog properly we are going to create a couple of placeholder articles, but for that we have to understand how we are going to generate the URL of the articles, because the structure of our folder is related to that task.
We add the blog
folder to the root of our project, and that is where we are going to add the articles we are going to write.
Since we have to write the articles in more than one language, what we are going to do is create a folder for each article, and inside that folder is where we are going to have our .md
files in the different languages we need.
This article is going to have to have the same URL in all the languages, so that we can see reflected in it the change of language.
To achieve this we are going to use the name of the folder we created for each article as the slug for it, something we will do later when we configure gatsby-node.js
.
For now, with that idea in mind, let’s create a couple of articles in order to move forward.
In my case I created 3 articles and my folder structure looked like this:
multilingual-blog/
├── blog/
│ ├── my-first-article/
│ │ ├── index.en.md
│ │ └── index.es.md
│ ├── another-article/
│ │ ├── index.en.md
│ │ └── index.es.md
│ └── a-third-one/
│ ├── index.en.md
│ └── index.es.md
├── ...
Now let’s see what goes inside each index.lang.md
.
As we are going to work with very simplified examples our articles only have to have a title, a date and its content.
Here are two examples:
File blog/my-first-article/index.en.md
---
title: My first article
date: "2022-03-17"
---
# Content of my first article
Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum, perferendis a delectus nemo ad iure.
Magni inventore ipsam ut nihil in maiores, culpa ipsum odit suscipit distinctio aut hic optio totam nemo tenetur, sunt assumenda?
File: blog/my-first-article/index.es.md
---
title: Mi primer artículo
date: "2022-03-17"
---
# Contenido de mi primer artículo
Lorem ipsum dolor sit amet consectetur adipisicing elit. Harum, perferendis a delectus nemo ad iure.
Magni inventore ipsam ut nihil in maiores, culpa ipsum odit suscipit distinctio aut hic optio totam nemo tenetur, sunt assumenda?
To learn more about Markdown it is best to read its official documentation.
Show all the articles on the blog page
We need to show the list of the articles we have written in our blog, but first, since we are using TypeScript, I think it is a good idea to define the interface that our article will have.
An interface for our articles
I’m going to create a new folder called models
inside src
where I’m going to save all the interfaces I need, starting with the one for my Articles, which will be in the Article.ts
file.
We said we had to have a title, a date and its content; these data we have already defined when we created our .lang.md
articles. When retrieving that data through GraphQL, we are going to receive it in an object called frontmatter
, so the interface of our model should look like this:
export interface IArticle {
id: string; // The id created by Gatsby
html: string; // Article content created by gatsby-source-filesystem
frontmatter: {
title: string; //
date: string; // Our variables
content: string; //
}
}
You can find more information about this on this Gatsby page.
Create our Blog page
When we set up gatsby-plugin-react-i18next
we had defined that our blog was going to be at /:lang/blog/:slug*
, which means that the URL is going to be example.com/lang/blog
, and our articles will be at example.com/lang/blog/my-first-article
.
Let’s start by creating our blog.tsx
file in the pages
folder inside src
.
In this file we have to include the GraphQL query where we collect all the articles of our website.
It will look something like this:
import React from "react";
import { graphql, PageProps } from "gatsby";
// Models
import { IArticle } from "models/Article";
type BlogPageProps = {
articles: {
nodes: IArticle[];
};
};
const BlogPage: React.FC<PageProps<BlogPageProps>> = ({ data }) => {
const articles = data.articles.nodes;
return (
<main>
<h1>Blog</h1>
{articles.map((article) => {
const { title } = article.frontmatter;
return (
<article key={article.id}>
<h2>{title}</h2>
<hr />
</article>
);
})}
</main>
);
};
export default BlogPage;
export const BlogPageQuery = graphql`
query BlogPageQuery {
articles: allMarkdownRemark {
nodes {
id
frontmatter {
title
date
}
}
}
}
`;
You can play around with the query builder that is in http://localhost:8000/__graphql.
Now if we go to http://localhost:8000/blog we should see a list of all the items we have, regardless of their language.
Let’s try to add the language to the URL.
Manage the languages of our web site
If we go to any page of our web and we see the console in the browser we will find that we are throwing the following error:
This is because i18next
has nowhere to get our translations from, because we have not written the query that picks them up from our locals
folder.
To fix that we simply add this query on all pages of our website, including the 404 error page.
# The Query name must be different for each page.
export const BlogPageQuery = graphql`
query BlogPageQuery($language: String!) {
locales: allLocale(
filter: { ns: { in: ["common"] }, language: { eq: $language } }
) {
edges {
node {
ns
data
language
}
}
}
}
`
It is also important that you write at least one object in the common.json
files of each language for it to be generated.
For example, in en/common.json
:
{
"go_to_my_blog": "Go to my blog",
"go_to_home": "Go to home",
"my_articles": "My articles",
"en": "English",
"es": "Español"
}
And in es/common.json
:
{
"go_to_my_blog": "Ir a mi blog",
"go_to_home": "Ir a inicio",
"my_articles": "Mis artículos",
"en": "English",
"es": "Español"
}
And to get it working we are going to create a layout for our site that allows us to change the language of our web on any page we are on.
If we are using TypeScript and we change the baseUrl
configuration to import files we have to add this code block to our gatsby-node.js
.
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
resolve: {
modules: [path.resolve(__dirname, "src"), "node_modules"],
},
});
};
Now we can create a folder called components
inside src
and inside I create a Layout.tsx
file with the following code.
import React from "react";
import { Helmet, useI18next, useTranslation } from "gatsby-plugin-react-i18next";
// Layout.module.css is a very basic style file that I made
import * as styles from "./Layout.module.css";
const Layout: React.FC = ({ children }) => {
// language is the active language on the page
// languages is an array with all available languages
// changeLanguage is the function that allows us to change languages
const { language, languages, changeLanguage } = useI18next();
// useTranslation() allows us to access our translations
const { t } = useTranslation();
return (
<div>
<Helmet>
<title>Multilingual Blog</title>
</Helmet>
<header className={styles.header}>Multilingual Blog</header>
<main className={styles.main}>{children}</main>
<footer className={styles.footer}>
{languages.map((lang) => (
<a
key={lang}
href="#"
className={lang === language ? styles.active : ""}
onClick={(e) => {
e.preventDefault();
changeLanguage(lang);
}}
>
{t(lang)}
</a>
))}
</footer>
</div>
);
};
export default Layout;
Let’s apply that layout to our home page.
We delete everything that came in pages/index.tsx
and create our own.
import React from "react";
import { graphql, PageProps } from "gatsby";
// Use the Link from gatsby-plugin-react-i18next
// allows us to set the urls to the language chosen by the user
// without the need to specify it on de url
import { Link, useTranslation } from "gatsby-plugin-react-i18next";
// Components
import Layout from "components/Layout";
const HomePage: React.FC<PageProps> = ({}) => {
const { t } = useTranslation();
return (
<Layout>
<Link to="/blog">
<h1>« {t("go_to_my_blog")} »</h1>
</Link>
</Layout>
);
};
export default HomePage;
export const HomeQuery = graphql`
query Home($language: String!) {
locales: allLocale(
filter: { ns: { in: ["common"] }, language: { eq: $language } }
) {
edges {
node {
ns
data
language
}
}
}
}
`;
The result
You can also do the same with the rest of the pages. I’ll show the case of blog.tsx
later in this post.
Configure gatsby-node.js
.
This part is the most important.
You need to understand and configure correctly the nodes of our articles that we are going to create.
Add new nodes to our articles
While when we retrieved our articles with graphql through the blog.tsx
page we were able to access their content, we have no way of programmatically knowing what language it is written in and we have not defined a slug, so let’s do that now.
To do this we are going to add new nodes with this information to the markdown files as they are created.
Gatsby provides us with the onCreateNode
function to accomplish this.
I will write the code for this function explaining it, but all the information can be found in the Gatsby documentation.
// createFilePath will help us to create the url of our article.
const { createFilePath } = require("gatsby-source-filesystem");
exports.onCreateNode = ({ node, actions, getNode }) => {
// createNodeField is the function that will create the new nodes.
const { createNodeField } = actions;
if (node.internal.type === `MarkdownRemark`) {
// createFilePath will return a string like this:
// /article-name/index.lang/
// I use the split(".") to separate the language from the rest.
const [slug, language] = createFilePath({ node, getNode }).split(".");
// Now we have:
// slug = /article-name/index
// language = lang/
// Now we create the node for the language.
createNodeField({
node,
// language will be the name of the field that we are going to pick up when querying
name: "language",
// language has the value of "lang/"
// this split function splits the string at the "/" at the end, returning only "lang".
value: language.split("/")[0],
});
// We create the node for the slug.
createNodeField({
node,
// Slug will be the name of the field that we are going to pick up when querying
name: "slug",
// Slug tiene el valor de "/article-name/index"
// This split function splits the string into each "/".
// We pick up only the second value of the returned array, wich is "article-name"
value: slug.split("/")[1],
});
}
};
This will create a new field called fields
that will group the new information that we have created.
More information about createNodeField
in the Gatsby documentation
Update the interface of our article
Now that we added the slug and the language to the nodes we are going to update that field field in the article interface.
It will look something like this:
export interface IArticle {
id: string; // The id created by Gatsby
html: string; // Article content created by gatsby-source-filesystem
frontmatter: {
title: string; //
date: string; // Our variables
content: string; //
}; fields: { slug: string; language: string; };
}
Create the URLs for our articles
Now that we have created the slugs for each article we are going to use them to create the dynamic URL for your pages with the createPages
function.
In the code I will discuss each thing, but to understand more about this function read the Gatsby documentation.
// Path is used to work with the paths of our files.
const path = require("path");
exports.createPages = async ({ graphql, actions, reporter }) => {
// With createPage we can create each page individually
const { createPage } = actions;
// Recogemos con graphql todos los archivos Markdown que hemos creado
const result = await graphql(`
{
articles: allMarkdownRemark(limit: 1000) {
nodes {
id
fields {
slug
language
}
}
}
}
`);
// If an error occurs, we report it and stop the execution.
if (result.errors) {
reporter.panicOnBuild(
`There was an error loading the page content`,
result.errors
);
return;
}
// articles will be an array of articles
const articles = result.data.articles.nodes;
// The template of our articles
// We have not created it yet, we are going to do it in the following step
const articleTemplate = path.resolve(`./src/templates/article.tsx`);
// We go through the articles array
articles.forEach((article) => {
// From the article, we only need the id, the slug and the language
const { id } = article;
const { slug, language } = article.fields;
// We create the page for each of them
createPage({
// path will be the URL of the article
path: `/${language}/blog/${slug}`,
// component is the template we are going to use
component: articleTemplate,
// In context there are the parameters that we can pass to our template
// They will be used to make the queries for each article.
context: {
id,
slug,
language,
},
});
});
};
Create and configure a template for our articles
If we start the site we will have a problem, because we still need to create the article.tsx
file in the templates
folder, so let’s create a very simple one for this case making use of the layout already created.
import React from "react";
import { graphql, PageProps } from "gatsby";
import { Link, useTranslation } from "gatsby-plugin-react-i18next";
// Models
import { IArticle } from "models/Article";
// Components
import Layout from "components/Layout";
type ArticlePageProps = {
article: IArticle;
};
const ArticlePage: React.FC<PageProps<ArticlePageProps>> = ({ data }) => {
const { t } = useTranslation();
const article = data.article;
const { title, date } = article.frontmatter;
const { language } = article.fields;
// Change the way the date is displayed according to the chosen language
const formattedDate = new Date(date).toLocaleDateString(language, {
year: "numeric",
month: "long",
day: "numeric",
});
return (
<Layout>
<h1>{title}</h1>
<small>{formattedDate}</small>
<main dangerouslySetInnerHTML={{ __html: article.html }} />
<hr />
<Link to="/blog">
<h1>« {t("go_to_my_blog")} »</h1>
</Link>
</Layout>
);
};
export default ArticlePage;
export const ArticlePageQuery = graphql`
query ArticlePageQuery($language: String!, $slug: String!) {
locales: allLocale(
filter: { ns: { in: ["common"] }, language: { eq: $language } }
) {
edges {
node {
ns
data
language
}
}
}
article: markdownRemark(
fields: { slug: { eq: $slug }, language: { eq: $language } }
) {
html
frontmatter {
date
title
}
fields {
slug
language
}
}
}
`;
This is reflected in the DOM:
.
Update blog.tsx
.
Finally we have to update the blog.tsx
file, in charge of rendering all the articles.
Unlike when we created it, we can now link each article to its page, as well as filter them according to the language we want.
import React from "react";
import { graphql, PageProps } from "gatsby";
import { Link, useTranslation } from "gatsby-plugin-react-i18next";
// Components
import Layout from "components/Layout";
// Models
import { IArticle } from "models/Article";
type BlogPageProps = {
articles: {
nodes: IArticle[];
};
};
const BlogPage: React.FC<PageProps<BlogPageProps>> = ({ data }) => {
const { t } = useTranslation();
const articles = data.articles.nodes;
return (
<Layout>
<h1>{t("my_articles")}</h1>
{articles.map((article) => {
const { title } = article.frontmatter;
const { slug } = article.fields;
return (
<Link to={`/blog/${slug}`} key={article.id}>
<h2>{title}</h2>
</Link>
);
})}
<Link to="/">
<h1>« {t("go_to_home")} »</h1>
</Link>
</Layout>
);
};
export default BlogPage;
export const BlogPageQuery = graphql`
query BlogPageQuery($language: String!) {
locales: allLocale(
filter: { ns: { in: ["common"] }, language: { eq: $language } }
) {
edges {
node {
ns
data
language
}
}
}
articles: allMarkdownRemark(
filter: { fields: { language: { eq: $language } } }
sort: { fields: frontmatter___date, order: DESC }
) {
nodes {
id
frontmatter {
title
date
}
fields {
slug
language
}
}
}
}
`;
I also sorted the files so that the most recent articles appear first.
**And with all this we would have finished setting up a very simple site in multiple languages, easy to scale and maintain.
Considerations
There are things still to do that we may need to do, for example if we want to paginate our articles and show only a limited amount per page we are going to have to create dynamic urlss** (/blog/
, /blog/2
, etc) for each of the languages we handle.
It is something that I am not going to do in this article because it is already too long, but maybe in the future I will share my way of doing it.
I can not leave without leaving the repository in which I worked making this article.
You can access it clicking here