Back
12.10.2024
#Jekyll
#liquid.js
#Web Development

Multi-language support in Jekyll

Image Source



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 hreflangs, which search engines welcome. Here are the hreflangs 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.