Multi-language support in Jekyll
I started looking for multi-lingual possibilities for the static webpage constructor called Jekyll, and did not find anything satisfactory. Since I wanted to offer my side project webdev-guide.net bilingually, I knew that I had to find a home-made solution. Also, after doing some research in the community, I realized that multi-language support is a common challenge among Jekyllians, which everyone tries to solve in their own best way. In the following I present my take on the Multi-Language-Support for Jekyll challenge:
Problem
Jekyll does not offer a native option for managing multilingualism. Therefore a custom-made solution is required.
Solution
Create a subfolder in _posts
with the language code of your preferred second language and replicate your primary folder structure in that created folder. Something like this:
├───basics
│ 2024-01-01-one.html
│ 2024-09-01-two.md
│ 2024-09-02-three.md
│
│
├───cybersecurity
│ 2024-01-01-one.html
│ 2024-09-02-two.md
│ 2024-09-26-three.md
│
├───javascript
| 2024-10-01-one.html
| 2024-10-02-two.md
| 2024-10-03-three.md
|
└───en
├───basics
│ 2024-01-01-one.html
│ 2024-09-01-two.md
│ 2024-09-02-three.md
│
├───cybersecurity
│ 2024-01-01-one.html
│ 2024-09-02-two.md
│ 2024-09-26-three.md
|
└───javascript
2024-10-01-one.html
2024-10-02-two.md
2024-10-03-three.md
The structure is simple: The folder /en/ mirrors the structure of the root directory _posts
, except for itself. This leads to simple handling via the URL, and I only have to perform URL checking to implement the routing logic.
The German article has the URL:
https://webdev-guide.net/basics/was-ist-frontend.html
The English article has the URL
https://webdev-guide.net/en/basics/what-is-frontend.html
The URLs differ only by the /en/
in which the English-language posts are located.
Now I insert an if-clause in the masthead of the Jekyll theme, which searches for a /en/
substring in the current URL. If the substring is found, Jekyll creates the variable "/en"
, which is prepended to all links in masthead.html
:
{% if page.url contains "/en/" %}
{% assign en = "/en" %}
{% endif %}
<a class="site-title" href="{{ en }}{{ '/' | relative_url }}">
{{ site.masthead_title | default: site.title }}
{% if site.subtitle %}<span class="site-subtitle">{{ site.subtitle }}</span>{% endif %}
</a>
<br>
<ul class="visible-links">
{%- for link in site.data.navigation.main -%}
<li class="masthead__menu-item">
<a
href="{{ site.url }}{{ en }}{{ link.url | relative_url }}"
{% if link.description %} title="{{ link.description }}"{% endif %}
{% if link.target %} target="{{ link.target }}"{% endif %}
>{{ link.title }}</a>
</li>
{%- endfor -%}
The practical thing about this is that we do not need else
because the variable en
remains empty if no /en/
exists in page.url
. The semantics of else is thus implicitly covered.
Let’s add flag icons from icons8 and style our language-switcher by adding CSS:
.multi_lang {
display: flex;
flex-direction: row;
flex-grow: 0;
}
.multi_lang > * > img {
width: 32px;
padding-bottom: 5px;
}
.bordered {
border-bottom: 2px solid #6f777d;
}
New Problem: SEO
Our second solution serves its purpose, but one thing can be said: The contributions that are in English have the same titles as the contributions that are in German. This is not a problem with language agnostic URL-parts such as:
/cybersecurity/backups.html
/cybersecurity/sql-injection.html
/basics/whatssl.html
… but for posts in which language influences semantics, my chosen approach worsens the SEO friendliness of the URL:
/basics/was-ist-php.html <!-- vs: /en/basics/what-is-php.html -->
/basics/wir-erstellen-eine-website.html <!-- vs: /en/basiss/we-create-a-website.html -->
To enable SEO-friendly URLs, I develop my approach further.
The SEO-friendly approach
Each German post receives the liquid variable en
, which contains the path to the English Post, and each English post receives the liquid variable de:
, which contains the path to the German Post. Bilingual posts can therefore differ as far as possible in the sense of the descriptor as long as the path to the alternative language post exists in the front matter.
With the new flexibility, there is more manual effort: Each post must be bilingual and linked accordingly. However, we can now realize the hreflang
tags in the < meta >
of our posts without detours. In the folder _includes
I create the file custom.html
and add the following:
<!-- HREFLANG -->
{% if page.url contains "/en/" %}
<link rel="alternate" hreflang="de-DE" href="{{ site.url | append: page.de | remove: '/en/' | remove: '.html' | append: '.html'}}" />
<link rel="alternate" hreflang="en-GB" href="{{ site.url | append: page.url | remove: '.html' | append: '.html' }}" />
{% else %}
<link rel="alternate" hreflang="de-DE" href="{{ site.url | append: page.url | remove: '/en/' | remove: '.html' | append: '.html'}}" />
<link rel="alternate" hreflang="en-GB" href="{{ site.url | append: page.en | remove: '.html' | append: '.html' }}" />
{% endif %}
<link rel="alternate" hreflang="x-default" href="{{ site.url | append: page.en | remove: '.html' | append: '.html' }}" />
<!-- END HREFLANG -->
Every post and every page includes custom.html. So every post and every page receives their dynamically generated hreflang
s, which search engines welcome. Here are the hreflang
s of the generated mail” we-create-a-website.html “or” we-create-a-website.html “:
<link rel="alternate" hreflang="de-DE" href="https://webdev-guide.net/basics/wir-erstellen-eine-website.html">
<link rel="alternate" hreflang="en-GB" href="https://webdev-guide.net/en/basics/we-create-a-website.html">
<link rel="alternate" hreflang="x-default" href="https://webdev-guide.net/en/basics/we-create-a-website.html">
I also add the language code in the < html >
tag of the individual posts and pages. Where the language code is defined is a subjective question. I define the language codes in _config.yml
:
- scope:
path: "_posts/"
lang: de
- scope:
path: "_posts/en/"
values:
lang: en
This gives all posts in the _posts /
folder the variable lang: de
. Posts in the subfolder _posts / en /
(as well as in all subfolders of / en /
) get the variable lang: en
. In the layout files in the folder _layouts
I now write:
<html lang="{{ page.lang | default: 'en' }}">
… which gives lang
the correct language code.
Conclusion
My chosen approach is not optimal, but good enough for the time being. What is missing are default values for posts that are only available in a single language, since the current approach triggers a 404 when users try to request a non-existent translation of the post. At the time of posting, I manage all posts bilingually, along with time expenses that need to be reduced.