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:

  1. Detects current language ({{ .Language.Lang }})
  2. Looks up corresponding data file
  3. Extracts content in appropriate language
  4. 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.