Plugin-free multilanguage Jekyll websites

Polyglot Jekyll

"Make it small. Make it dead simple." —Adam Morse

Polyglot Jekyll serves both as starting point for building a multilanguage website in vanilla Jekyll, and as instructional demo for my approach.

The boilerplate code is intentionally bare-bone, just the few files necessary for Jekyll to build the website and minimal CSS for the demo. There's no build dependency except for Jekyll, no SASS compiling, no browser-sync, no gulpfile.js, no package.json.

Just clone the repo and run jekyll build to build locally.

The approach

The underlying principle is a well-known, universally applicable best practice: separation of content from presentation. Jekyll allows to extract a layout into separated files that support basic logic through the Liquid templating engine.

With Jekyll, the file system structure is the website structure. We deal with multilanguage content by mirroring our site's structure in language-dedicated directories. Our default language will be at the root level, and every other language we want to support will have its own directory. Here's how a basic structure might look:

 index.md       |   /            
 about.md       |   /about       
 contact.md     |   /contact     
 de/            |                
 ├── index.md   |   /de          
 ├── about.md   |   /de/about    
 └── contact.md |   /de/contact  
 it/            |                
 ├── index.md   |   /it          
 ├── about.md   |   /it/about    
 └── contact.md |   /it/contact  

Note: permalink: pretty needs to be set in _config.yml for the URL structure to be generated as above.

Once we have our starting structure set up, we need to set some metadata in every page using the YAML frontmatter. Besides the usual variables like layout and title, we need to set a language in every page and a handle in every page other than index pages:

# In /about.md:
layout: default
title: About
language: en
handle: /about # same as in /de/about.md and /it/about.md

The reason for these two extra variables will become clear in a moment. Now let's deal with the content.

Site-wide content

We put content that needs to present across the whole website (like the one you typically find in header and footer) in our _config.yml file. Dealing with strings in different languages is possible with a hash:

# In _config.yml
  en: Plugin-free multilanguage Jekyll websites
  de: Plugin-freie vielsprachige Jekyll Webseiten
  it: Siti multilingue in Jekyll, senza plugin.

We then use Liquid's bracket notation in the layout file to access the values in our hash. The language variable in the current page's frontmatter ensures that we grab the correct string from _config.yml:

<!-- Data from _config.yml get stored in the `site` global variable -->
<meta name="description" content="{{ site.description[page.language] }}">

Page-specific content

With the structure described above is simple to add copy to every page, just write it in markdown in every file after the frontmatter, and use the special variable {{ content }} to grab it from the layout file.

You often need something more flexible for your average website though. You probably have boxes, buttons, forms and other UI elements, or images with alt texts that also need translation. You can deal with this by expanding the frontmatter in the single page files with the variables you need:

# In /index.md
see-on-github: See on GitHub
tweet-this: Tweet this
# In /de/index.md
see-on-github: Auf GitHub sehen
tweet-this: Twittern
# In /it/index.md
see-on-github: Vedi su GitHub
tweet-this: Twitta

Thanks to this method, we can keep our layout file nice and tidy:

<!-- Data from page files get stored in the `page` global variable -->
<a class="button" href="#">{{ page.see-on-github }}</a>
<a class="button" href="#">{{ page.tweet-this }}</a>

The system described above is incredibly flexible and will cover most of the use cases.

Dealing with a navigation menu is a simple matter. But, since our website has every page mirrored for every language, we need to add a conditional in the for loop to make sure that only the pages in the page's current language get picked up, otherwise Jekyll will happily include every page in our project without regard to the language.

  {% for p in site.pages %}
    {% if p.language == page.language %} 
      <a href="{{ p.url | prepend: site.baseurl }}">
        {{ p.title }}
    {% endif %}
  {% endfor %}

Language switch

The language switch is slightly trickier to implement. Let's get the easy part out of the way first. Here's how we deal with switching language from an index page (i.e. a homepage):

<nav class="language-switcher">
  {% if page.name contains "index" %}
    <a href="{{ site.baseurl }}/">EN</a>
    <a href="{{ site.baseurl | append: "/de" }}">DE</a>
    <a href="{{ site.baseurl | append: "/it" }}">IT</a>
  {% endif %}

Since index pages sit at the first level of their directories, we just need to append the language to the base url. But what if we want to switch language while being on another page? That's where our handle variable in the frontmatter comes into play:

<nav class="language-switcher">
  {% if page.name contains "index" %}
    <a href="{{ site.baseurl }}/">EN</a>
    <a href="{{ site.baseurl | append: "/de" }}">DE</a>
    <a href="{{ site.baseurl | append: "/it" }}">IT</a>
  {% else %}
    <a href="{{ site.baseurl | append: page.handle }}">EN</a>
    <a href="{{ site.baseurl | append: "/de" | append: page.handle }}">DE</a>
    <a href="{{ site.baseurl | append: "/it" | append: page.handle }}">IT</a>
  {% endif %}

On every page other than index pages, every switch link gets the language and the page handle appended. If we're on the page with the /about handle, the generated HTML will look like so:

<a href="/about">EN</a>
<a href="/de/about">DE</a>
<a href="/it/about">IT</a>

Note: In navigation and language switch, you only need the prepend: site.baseurl bit if your site that doesn’t sit at the root of the domain. See this to learn why.

This wraps up our brief excursus of Polyglot Jekyll. Make sure to clone this repo and experiment with it locally to get a hang of how everything works together. If you want to check out the final result, have a look at the online demo.

The approach should be solid enough and easily extensible to more complex websites. The next time you need to build a multilingual website, give it a shot. You might find that you don't need PHP, CMS's and databases to deal with it after all.


  • Still new to Jekyll? The official docs are the best place to start.
  • When you feel a bit more comfortable, this cheat sheet put together by CloudCannon is the go-to reference for all things Jekyll.
  • YAML is a flexible and powerful syntax to structure your data. Here's the best overview available.
  • To go in-depth into the Liquid templating engine, check out Shopify's official reference or these docs.