[Multilang] Code snippet for hreflang and x-default

If you have a multi-language Hugo site, <link rel="" hreflang="" href="" /> is highly encouraged.

The code

In your baseof.html or head.html (or whichever filename your theme is using):

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

In your sitemap.xml file, add the following within <url></url>

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

Example: This is my sitemap.xml file (with additional edits):

{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>" | safeHTML }}
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns:xhtml="http://www.w3.org/1999/xhtml">
  {{- range .Data.Pages -}}
  {{ if .Permalink }}
  <url>
    <loc>{{ .Permalink }}</loc>
    {{- if not .Lastmod.IsZero }}
    <lastmod>{{ safeHTML ( .Lastmod.Format "2006-01-02T15:04:05Z" ) }}</lastmod>
    {{- end -}}
    {{- with .Sitemap.ChangeFreq }}
    <changefreq>{{ . }}</changefreq>
    {{- end -}}
    {{- if ge .Sitemap.Priority 0.0 }}
    <priority>{{ .Sitemap.Priority }}</priority>
    {{- end -}}
    {{- if .IsTranslated -}}
    {{ range .AllTranslations }}
    <xhtml:link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}" />
    {{- end -}}
    {{ range first 1 .AllTranslations }}
    <xhtml:link rel="alternate" hreflang="x-default" href="{{ .Permalink }}" />
    {{- end -}}
    {{ end }}
  </url>
  {{- end -}}
  {{ end }}
</urlset>

BONUS: If you have a separate list.atom.xml file instead of the default rss.xml, add this as a child of <feed></feed> or before <entry>:

{{- with .OutputFormats.Get "Atom" }}
  <link rel="self" type="{{ .MediaType.Type | html }}" hreflang="{{ $.Language.Lang }}" href="{{ .Permalink | safeURL }}" />
{{- end }}
{{- if .IsTranslated -}}
  {{ range .Translations }}
    <link rel="alternate" type="application/atom+xml" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}feed.xml" />
  {{- end -}}
  {{ range first 1 .AllTranslations }}
    <link rel="alternate" type="application/atom+xml" hreflang="x-default" href="{{ .Permalink }}feed.xml" />
  {{- end -}}
{{ end }}
{{- range .AlternativeOutputFormats }}
  <link rel="alternate" type="{{ .MediaType.Type | html }}" hreflang="{{ $.Language.Lang }}" href="{{ .Permalink | safeURL }}" />
{{- end }}

See it in action:

  1. Compare view-source The YOOki Chronicles and view-source YOOki 연대기
    You should see the same:

    <link rel="alternate" hreflang="en-ph" href="https://im.YourOnly.One/yuki/" />
    <link rel="alternate" hreflang="ja" href="https://im.YourOnly.One/yuki/ja/" />
    <link rel="alternate" hreflang="ko" href="https://im.YourOnly.One/yuki/ko/" />
    <link rel="alternate" hreflang="x-default" href="https://im.YourOnly.One/yuki/" />
    
  2. Compare view-source Is a derivative of a Public Domain work, fanfiction? and view-source パブリックドメインの派生物は作品、ファンフィクションですか?
    You should see the same:

    <link rel="alternate" hreflang="en-ph" href="https://im.YourOnly.One/yuki/derivative-public-domain-fanfiction-202197/" />
    <link rel="alternate" hreflang="ja" href="https://im.YourOnly.One/yuki/ja/derivative-public-domain-fanfiction-202197/" />
    <link rel="alternate" hreflang="ko" href="https://im.YourOnly.One/yuki/ko/derivative-public-domain-fanfiction-202197/" />
    <link rel="alternate" hreflang="x-default" href="https://im.YourOnly.One/yuki/derivative-public-domain-fanfiction-202197/" />
    
  3. Compare sitemaps view-source English | Nihongo | Hangugeo

  4. Compare atom feed view-source English | Japanese | Korean

Notes

  1. x-default will always point to the first language listed in your config file. Make sure the first language in your config file is your default/fallback language (usually English).
  2. Atom feed: The <link> element is allowed in Atom 1.0 feeds. Personally, I do not know if there are readers and/or services that consume that extra data.
    • Do not add it in Hugo’s default rss.xml as that is a combined RSS2.0 and Atom1.0 template. RSS2 will reject the <link> element and will produce an error. But again, I am not aware of any reader and/or service which consumes that extra information.
  3. What is important is your HTML files and sitemap.
  4. In some sites they mention that rel=canonical must match one of the hreflang values. It is not in Google’s official document, what their document emphasized on is that the current language must also show up in the hreflang list in that same article or page.
    • This makes sense because Google, and other search engines, also use rel=canonical for syndication sites. Syndication sites, for example news sites, display articles verbatim. Without a rel=canonical pointing to the original/source article the syndication site and/or the original site will be marked as “duplicate”.
    • In other words, there are cases wherein rel=canonical will be different than any of the hreflang values listed.
    • Example document stating that rel=canonical must match one of the hreflang values: Wikipedia: hreflang 2.4 Common Mistakes.

More about hreflang and x-default, and rel=canonical


I hope it helped! Enjoy life!

Shalom!

~YourOnly.One

8 Likes

In my case, this did not work. Using first 1 in the range picked up the french language, which was the second one in the config file, maybe because I had a separate language config file.

I tried to use the defaultContentLanguage defined in config to check if the language in the loop was the default one, but it seems there is no variable exposing it, so I ended up hardcoding the language in the code for now.

{{ if .IsTranslated }}
    {{ range .Page.AllTranslations }}
    <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
      {{ if eq .Language.Lang "en" }}
        <link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
      {{ end }}
    {{ end }}
  {{ end }}

Do you have a link to your source? I’m curious. :slight_smile:

I’m using this alternate approach to pick the first language as the default one:

{{- if .IsTranslated -}}
{{- range $index, $trans := .AllTranslations -}}
{{- if eq $index 0 }}
<link rel="alternate" hreflang="x-default" href="{{ $trans.Permalink }}">
{{- end }}
{{- if not (eq $trans.Language.Lang $.Page.Language.Lang) }}
<link rel="alternate" hreflang="{{ $trans.Language.Lang }}" href="{{ $trans.Permalink }}">
{{- end }}
{{- end -}}
{{- end }}
1 Like

If I may ask - do we need to provide those alternate links in HTML code AND sitemap.xml or sitemap.xml alone is enough?

Here (link), for instance, Google says or:

annotate cluster of pages using rel-alternate-hreflang using sitemaps OR using HTML link.

Thanks.

A post was split to a new topic: Question about hreflang attribute