Bilingual with Hugo: How I Approached It
Multilingual setup in Hugo sounds simple at first: define languages in config, translate content, done. In practice, issues hide in the details.
Why bother with multilingual content at all if browsers can translate pages live? For me, the answer is simple: I think in German, writing in German is more natural. But live translation is not enough for me, because I want full control over tone, terminology, and nuance in English.
For a moment, I considered writing in German, translating to English, and then throwing the German original away. That felt wrong. I chose a clear path: the blog stays German, with a deliberately maintained English version.
In this post, I share how I extended Meadow Lark to German and English, what to watch out for, and which errors I hit.
1) A Solid Base in Hugo Config
The first step is a clean language configuration in hugo.yaml.
The key points are:
- Set defaultContentLanguage to ‘de’
- Define both languages under languages
- Maintain menu entries per language
- Use a consistent date format per language
I set the theme entry explicitly. Without an active theme, layouts can be missing or pages can appear blank.
2) Use Language File Pairs for Content
I use paired files:
- about.md and about.en.md
- projects.md and projects.en.md
- posts/slug.md and posts/slug.en.md
This makes mapping unambiguous and allows Hugo to connect translations cleanly.
Important: each language version needs its own front matter and a meaningful summary. The summary should be written manually, not auto-generated.
3) Language Switcher in the Template
The switcher lives in a reusable navigation component. The basic principle:
- If a translation exists for the current page, link to that page directly
- If not, link to the other language’s homepage
That second part matters, so users do not end up in a dead end.
4) Typical Errors That Happen Fast
Error 1: Theme Not Active
Symptom: The homepage looks blank or layouts are incomplete.
Cause: The theme entry is missing in hugo.yaml.
Fix: Set theme explicitly so base layouts are loaded.
Important: theme belongs at the top level in hugo.yaml, not under languages, params, or outputs.
baseURL: "https://meadowlark.care"
theme: "hugo-bearblog"
defaultContentLanguage: de
defaultContentLanguageInSubdir: false
Error 2: API Differences Between Hugo Versions
Symptom: Everything works locally, but the cloud build fails in templates because two Hugo versions are involved.
In my case, local was Hugo 0.160 while Cloudflare built with Hugo 0.146. A newer template API therefore worked locally.
Fix: Pin versions: local and cloud use the same Hugo version (for example 0.160.1), then deploy. If that is not possible, adjust the language/i18n logic to more compatible constructs. Make sure to rebuild locally after that, then redeploy.
In practice, I pinned in two places: Cloudflare with HUGO_VERSION=0.160.1 and locally via a .tool-versions file containing hugo 0.160.1. That keeps local and online builds aligned.
Error 3: Language-Dependent URLs in Footer or Menu
Symptom: RSS links or language links point to the wrong language or to 404s.
Fix: Build links via Hugo output formats and RelPermalink instead of hardcoding paths.
Error 4: Translations Exist but Are Not Linked
Symptom: The EN file exists, but the switcher does not show it.
Cause is inconsistent file naming or missing counterpart files.
Fix: Keep language pair naming consistent and compare front matter.
5) My Minimal Test Set Before Push
Before I push, I do a quick check:
- Local hugo build without errors
- Language switcher on home and post pages
- Click DE/EN menu links
- Test RSS links in both languages
The local-vs-cloud version mismatch taught me one thing: a green local build is not always enough.
Conclusion
Bilingual setup with Hugo is manageable when configuration, file naming, and templates work together cleanly.
The biggest pitfalls are not the content itself, but small infrastructure details: theme activation, stable template APIs, and robust URL generation.
Once those three are in place, Hugo i18n becomes pretty relaxed.