Hugo Multi-Lang: Internal Permalinks
Contents
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 */ -}}
Solution: Language-Dependent Link Prefixes#
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:
- Checks if links are “external” - that is, leading away from my site. These are assigned a separate window and the
↗indicator. - Checks if the link points to a heading “anchor.” See below for details.
- 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
Correctly resolving permalinks to headings#
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.