Zola Multilingual Setup

Optimizing SEO and adding a language switch on a partially translated website

2025-08-20

Zola is the static site generator that powers this website. It derives routing from the structure of content Markdown files, and supports multilingual websites out of the box. In this article I share a few tricks I used to configure it properly.

Intl in Zola

In zola, there is content (markdown files in the content/ directory) and templates (html files defining how to render the content). For example, the content for this page is written in French at /content/blog/2025-08-20-zola-intl-routing.md and
in English at /content/blog/2025-08-20-zola-intl-routing.en.md.

When a general text is needed in the templates (not in the content), we can define translations in config.toml. For instance, the main links on the top of the page are not defined in a content page, but in the template that renders all pages with a navbar. Then we render links while specifying that they should keep the current language.

<a href="{{ get_url(path="@/projects/_index.md", lang=lang) }}">{{ trans(key="projects", lang=lang) }}</a>
<a href="{{ get_url(path="@/blog/_index.md", lang=lang) }}">{{ trans(key="blog", lang=lang) }}</a>
<a href="{{ get_url(path="@/about.md", lang=lang) }}">{{ trans(key="about", lang=lang) }}</a>

Here, lang is the current language you use, get_url will resolve the right URL for the link, and trans() will get the text in the right language, as defined in the config.toml.

Canonical and Alternate

Good SEO practices suggest indicating on each page of a multilingual website

In zola, this may be done using the section.translations (if a directory of pages) or page.translations (if a single page). We first need to identify the permalink of each page. This can be done in the base.html template (from which all other templates are derived)

{% set translations = section.translations | default(value=page.translations) %}
{% if translations %}
    {% set en = translations | filter(attribute="lang", value="en") | first %}
    {% if en %}
        {% set en_url = en.permalink %}
    {% endif %}

    {% set fr = translations | filter(attribute="lang", value="fr") | first %}
    {% if fr %}
        {% set fr_url = fr.permalink %}
    {% endif %}
{% endif %}

Then the canonical URL is:

`Zola Multilingual Setup`` {% if lang == "en" %} {% set canonical_url = en_url %} {% else %} {% set canonical_url = fr_url %} {% endif %}


In the head then: 

{% if fr_url %} {% endif %} {% if en_url %} {% endif %}


Don't forget to add the `lang` attribute to the html tag: 

```

Pagination

Sections are defined by having a _index.md in a directory, with some metadata. For example, a section may show a paginated list of projects, or blog posts. This is the case for the blog section of this site. In this case, the page can be paginated. When pagination is enabled, Zola provides a paginator variable. The en.permalink would then not be enough to link to the other language page. We need to slightly adapt our snippet:

{% set translations = section.translations | default(value=page.translations) %}
{% if translations %}
    {% set en = translations | filter(attribute="lang", value="en") | first %}
    {% if en and paginator and paginator.current_index > 1 %}
        {% set en_url = en.permalink ~ "page/" ~ paginator.current_index %}
    {% elif en %}
        {% set en_url = en.permalink %}
    {% endif %}

    {% set fr = translations | filter(attribute="lang", value="fr") | first %}
    {% if fr and paginator and paginator.current_index > 1 %}
        {% set fr_url = fr.permalink ~ "page/" ~ paginator.current_index %}
    {% elif fr %}
        {% set fr_url = fr.permalink %}
    {% endif %}
{% endif %}

This way, when on the second page of the English blog section, the alternate is the second page of the French blog section.

Additionally, we can specify that this page is part of a paginated series by adding metadata tags link with rel=prev and rel=next:

{% if paginator.previous %}
  <link rel="prev" href="{{ paginator.previous }}">
{% endif %}

{% if paginator.next %}
  <link rel="next" href="{{ paginator.next }}">
{% endif %}

Lang switch

We can now easily implement a language switch. We just need to add a link to fr_url when language is English, and a link to en_url when the language is French.

{% if lang == "en" and fr_url %}
    <div class="lang-switch">
        <a href="{{ fr_url }}" aria-label="Switch to French">
            FR
        </a>
    </div>
{% elif lang == "fr" and en_url %}
    <div class="lang-switch">
        <a href="{{ en_url }}" aria-label="Switch to English">
            EN
        </a>
    </div>
{% endif %}

If the current language is the only one available, the other URL will be null and the switch will not be displayed. You could change this logic easily to return to home, or to just disable the link but keep it visible.

Conclusion

Configuring a multilingual website in Zola has a few quirks that weren’t obvious to me when I first red the multilingual documentation. This article is my attempt to capture what I learned along the way. I hope it helps, and I’d be happy to hear your suggestions for improving this approach.