Sitio multilenguaje: un solo árbol de contenido
How to implement a multilingual Hugo site using a single content tree and data files
After changing the site to HUGO last year and getting a better understanding of this technology/framework, I wanted to solve some issues and see if it’s possible to serve multilingual content in a simplified way, that is, from a single file tree, without multiplying content branches.
The Conventional Way
By default, HUGO proposes creating a content tree for each language. This means creating a similar structure for each one:
content/
├── es/
│ ├── posts/
│ └── series/
├── en/
│ ├── posts/
│ └── series/
└── pt-br/
├── posts/
└── series/
In cases where content is very different between languages, this approach is appropriate. That’s not my case.
The biggest penalty comes from maintaining 3 versions of each file.
The Series
Another thing I wanted to do is create simple article series, something that helps maintain coherent navigation and information flow between a set of articles related to a topic. This post itself is part of a series, and I’ll talk about this in another article later on.
Data Files + Smart Templates
To solve the multilingual content, I finally implemented a hybrid system that uses YAML data files for multilingual content and Hugo templates that automatically select the correct language.
Final Structure
content/
└── 20250810_my_post.md # Only metadata
data/
└── 20250810_my_post.yaml # Content in 3 languages
layouts/
├── _default/
│ ├── single.html # Renders from data files
│ └── summary.html # Multilingual excerpts
└── partials/
└── content-renderer.html # Language selection logic
Content File (.md)
The .md file only contains metadata and a reference:
---
title: 'Moxon Antenna for 6m'
date: 2025-08-10
categories: ["Radio"]
tags: ["Antenna", "VHF"]
---
This post uses data files to show different content based on user language.
Data File (.yaml)
All the actual content is in the YAML file:
title:
es: 'Antena Moxon para 6 metros'
en: 'Moxon Antenna for 6 meters'
pt: 'Antena Moxon para 6 metros'
content:
es: |
La Moxon es una direccional simple de dos elementos...
en: |
The Moxon is a simple two-element directional antenna...
pt: |
A Moxon é uma antena direcional simples de dois elementos...
I’m not convinced about this structure. Right off the bat, I think it’s not necessary, and I’m not sure if it’s convenient, to use the file in /data for translated content, I’ll probably go back to having all language content in the .md file.
Rendering Template
The template automatically detects the language and renders the appropriate content:
{{ $currentLang := .Language.Lang }}
{{ $contentKey := .File.BaseFileName }}
{{ $dataFile := index .Site.Data $contentKey }}
{{ if $dataFile }}
{{ $title := index $dataFile.title $currentLang }}
{{ $content := index $dataFile.content $currentLang }}
<h1>{{ $title }}</h1>
<div class="content">
{{ $content | markdownify }}
</div>
{{ end }}
System Benefits
✅ Simplified Maintenance
- Single file per post (the .yaml)
- Single URL serving 3 languages
- No duplication of directory structure
✅ SEO Optimized
- Clean URLs:
/es/post/,/en/post/,/pt-br/post/ - Automatic hreflang between versions
- Unique content per language (not duplicated)
✅ User Experience
- Instant language switching (same URL)
- Navigation consistency
- Smart fallback if translation is missing
✅ Efficient Workflow
- Write in Spanish
- System automatically translates to EN/PT-BR
- Single publication for all 3 languages
Technical Implementation
The heart of the system is in the template logic that:
- Detects current language (
{{ .Language.Lang }}) - Looks up corresponding data file
- Extracts content in appropriate language
- Renders with fallback if translation is missing
{{/* Look for content in current language */}}
{{ $content := index $dataFile.content $currentLang }}
{{/* Fallback to Spanish if translation doesn't exist */}}
{{ if not $content }}
{{ $content = index $dataFile.content "es" }}
{{ end }}
{{ $content | markdownify }}
Results
The site now automatically serves content in Spanish, English and Portuguese from a single codebase. Readers can access articles in their preferred language, but I only have to maintain one set of files.
git hooks
Since the main advantage of this is that publishing a post is very streamlined (new file, write, git add, git commit, git push), doing translations is definitely an additional and significant friction point. That’s why I’m going to implement a script that uses some AI API to do the translations, we’ll see how that goes.
Site URLs:
The system works transparently for the end user, but greatly simplifies maintenance work.