Anyone building a multilingual blog with Hugo might be familiar with the following problem:

Internal links in articles across different languages point only to pages in the default language.

Configuration#

The issue stems from the following setting:

# hugo.yml

defaultContentLanguage: "de"
defaultContentLanguageInSubdir: false
disableDefaultLanguageRedirect: true

My default language is German, and the corresponding articles are built into the blog’s root directory. No redirect to the /de language directory takes place.

The directory structure is as follows:

├── content
│   ├── de
│   │   ├── about.md
│   │   ├── announcements
│   │   ├── legal.md
│   │   ├── posts
│   │   ├── privacy.md
│   │   └── projects
│   └── en
│       ├── about.md
│       ├── legal.md
│       ├── posts
│       ├── privacy.md
│       └── projects

The Problem with the Default Language#

I make articles in my default language accessible via URLs like example.com/my-post and example.com/my-2nd-post/ by using the slug parameter. In the secondary language, however, they appear as example.com/en/my-post and example.com/en/my-2nd-post/.

If I want to link two articles internally, the link always points to the article in my default language.

# English version of my post
# example.com/en/my-post
---
slug: my-post

---
Post's English content
[2nd](/my-2nd-post)

As a result, instead of the desired /en/my-2nd-post, I end up at the German version: /my-2nd-post. I find this annoying, as it would require me to manually adjust every link in every article. Unfortunately, Hugo does not seem to provide a built-in solution for this. So, I am creating a rule for how links should be rendered:

{{- /* /layouts/_markup/render-link.html */ -}}

Here is what needs to happen: When a relative link within the site is encountered during the build process, a language prefix (such as /en) should be prepended to the link only if the language of the page being rendered is not the default language (since I do not need a language prefix for the default language, given that defaultContentLanguageInSubdir is set to false).

Retrieving the Default Language#

But how do I access the default language? The defaultContentLanguage parameter is not accessible within templates. After some research, I found a suitable solution to the problem in the Hugo community: The default language is usually the first language in the weight-based index (which determines the order in which languages appear): index .Page.Site.Home.AllTranslations 0. When I access this, the “first” one is my default language.

Code for render-link.html#

{{- /* /layouts/_markup/render-link.html */ -}}

{{- $url := .Destination | safeURL -}}
{{- $currentLang := .Page.Language.Locale -}}

{{- $is_external := or (strings.HasPrefix $url "http://") (strings.HasPrefix $url "https://") -}}
{{- $is_anchor := strings.HasPrefix $url "#" -}}
{{- $is_asset := strings.HasPrefix $url "/assets/" -}}
{{- $is_lang_prefixed := strings.HasPrefix $url (printf "/%s/" $currentLang) -}}

{{- if $is_anchor -}}
  {{- $url = printf "%s%s" .Page.RelPermalink $url -}}
{{- else -}}
  {{- if not $is_external -}}
    {{- if and (not $is_lang_prefixed) (not $is_asset) (not .Page.Language.IsDefault) -}}
      {{- $url = printf "/%s%s" $currentLang $url -}}
    {{- end -}}
  {{- end -}}
{{- end -}}

{{- /* Add / to internal paths that have no file extension */ -}}
{{- if and (not $is_external) (not $is_anchor) (not $is_asset) -}}
  {{- $parsed := urls.Parse $url -}}
  {{- if and $parsed (not (strings.HasSuffix $parsed.Path "/")) (eq (path.Ext $parsed.Path) "") -}}
    {{- $url = printf "%s/" $url -}}
  {{- end -}}
{{- end -}}

<a href="{{ $url | relURL }}"{{ if $is_external }} target="_blank" rel="noopener noreferrer" class="external-link"{{ end }}>
  {{- .Text | safeHTML -}}
  {{- if $is_external -}}
    <span class="external-link-icon" aria-hidden="true">↗</span>
  {{- end -}}
</a>

As long as the default language and the weight index match, meaning the default language appears at the very top, this works perfectly well.

Additional functions#

Here is what else the code does:

  1. Checks if links are “external” - that is, leading away from my site. These are assigned a separate window and the ↗ indicator.
  2. Checks if the link points to a heading “anchor.” See below for details.
  3. Checks if the link points to a (media) file or “asset.” Asset links are not modified. If it is a heading link, it is expanded to the full URL if necessary. This follows this pattern: #heading --> /<opt_lang>/<opt_category>/<title>/#heading

I have also found a solution for highly specific permalinks within a page. Let’s assume I have headings in different languages:

---
# German
# content/de/posts/yyyy-mm-dd-hugo-create-language-specific-permalinks.md

description: Mit _markup interne Links richtig routen
title: "Hugo Multi-Lang: Interne Permalinks"
slug: hugo-create-language-specific-permalinks
---

## Richtig auflösende Permalinks auf Überschriften {#permalinks}
Lorem Ipsum...
---
# English
# /content/en/posts/yyyy-mm-dd-hugo-create-language-specific-permalinks.md

description: Routing internal links with _markup
title: "Hugo Multi-Lang: Internal Permalinks"
slug: hugo-create-language-specific-permalinks
---

## Correct Permalinks to headings {#permalinks}
Lorem Ipsum...

Then, clicking this link back to the #permalinks heading should take the user back to the heading, regardless of the language. The key to this solution is introducing a heading definition (Heading-ID) in Markdown: in this case, {#permalinks}.

It doesn’t work quite yet.#

This is because #permalinks points to <blog's baseurl>/#permalinks, meaning the link to the current page is missing. But we can fix that, too: I’ll extend the link renderer with a few lines of code to prepend the current page URL.

Code for render-link.html with heading support#

{{- $is_anchor := strings.HasPrefix $url "#" -}}

{{- if $is_anchor -}}
{{- $url = printf "%s%s" .Page.RelPermalink $url -}}
{{- end -}}

Now, (#permalinks) expands to /hugo-create-language-specific-permalinks/#permalinks, so the link finally leads to the correct destination.