NNicheStackTutorials
← All tutorials
Hugo

Hugo Multilingual Sites with Subfolder URLs (/en/, /es/)

Configure languages, translation links, and hreflang without subdomain chaos. Battle-tested for 3 locales on Vercel.

·14 min read·Hugo
Hugo Multilingual Sites with Subfolder URLs (/en/, /es/) cover

Tested on: Hugo 0.139.4, 3 languages (en, es, de), Vercel + Cloudflare URL shape: example.com/en/blog/post-slug/ — no www.es.example.com

Why subfolders beat subdomains

  • One TLS cert, one Search Console property (with path rules)
  • Shared assets cache
  • hreflang alternates are simpler

hugo.toml language block

defaultContentLanguage = "en"
defaultContentLanguageInSubdir = true

[languages.en]
  languageCode = "en-us"
  languageName = "English"
  weight = 1
  title = "My Blog"
  [languages.en.params]
    description = "English tutorials"

[languages.es]
  languageCode = "es-es"
  languageName = "Español"
  weight = 2
  title = "Mi Blog"
  [languages.es.params]
    description = "Tutoriales en español"

[languages.de]
  languageCode = "de-de"
  languageName = "Deutsch"
  weight = 3

defaultContentLanguageInSubdir = true is the key — without it English lives at / and others at /es/, breaking symmetry.

Content file layout

content/
├── en/blog/my-post.md
├── es/blog/my-post.md
└── de/blog/my-post.md

Each file:

---
title: "My Post"
translationKey: "my-post-2026"
date: 2026-05-01
---

Same translationKey ties translations together for .Translations and hreflang partials.

hreflang partial

layouts/partials/hreflang.html:

{{- if .IsTranslated -}}
  {{- range .AllTranslations -}}
    <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}" />
  {{- end -}}
  <link rel="alternate" hreflang="x-default" href="{{ .Site.Home.Permalink }}" />
{{- end -}}

Add to layouts/partials/head.html.

Language switcher

<nav aria-label="Language">
  {{- range site.Languages -}}
    {{- $url := "/" -}}
    {{- if $.IsTranslated -}}
      {{- range $.Translations -}}
        {{- if eq .Language.Lang .Lang }}{{ $url = .RelPermalink }}{{ end -}}
      {{- end -}}
    {{- else if eq .Lang $.Lang -}}
      {{- $url = $.RelPermalink -}}
    {{- end -}}
    <a href="{{ $url }}" hreflang="{{ .Lang }}"
       {{- if eq .Lang $.Lang }} aria-current="page"{{ end -}}
    >{{ .LanguageName }}</a>
  {{- end -}}
</nav>

Errors I hit

WARN  found duplicate translation with key "my-post"

Two Spanish files shared the same translationKey but different slugs. One slug per translationKey per language.

404 on /es/ after deploy

Vercel vercel.json rewrite sent everything to index.html (SPA mode). Static Hugo output must be served as files:

{
  "buildCommand": "hugo --minify",
  "outputDirectory": "public"
}

Remove SPA rewrites.

menus per language

hugo.toml:

[[languages.en.menu.main]]
  name = "Blog"
  url = "/blog/"
  weight = 1

[[languages.es.menu.main]]
  name = "Blog"
  url = "/blog/"
  weight = 1

URLs are relative to language root automatically.

Verify

hugo --minify
ls public/en/blog/ public/es/blog/ public/de/blog/
curl -s http://localhost:1313/en/blog/my-post/ | grep hreflang

Result

3 locales, 1 repo, 1 deploy pipeline. Organic ES traffic +18% after hreflang fix.

Tags: Hugoi18nMultilingualSEO