×
Home RSS feed info
Make search engines aware of the localized versions of your website.

SEO - Self-referencing hreflang on multilingual website

SEO - Self-referencing hreflang on multilingual website

On multilingual websites, all language versions, including a self-reference and if possible an x-default reference, must be indicated in the header for good SEO. I explain the necessary Hugo source code for this in this post. With link rel=“alternate” hreflang="" href="" this succeeds.

Important points to consider when using alternate links:

  • Every page that uses alternate links needs a link back to itself with the appropriate language.
  • Every page that is linked to with a language must link back to the linking page with the corresponding language (A -> B, B -> A).
  • Show alternative versions of a page with other languages.
  • Designate an x-default page as a default or fallback page that is displayed to users when no other language variant is appropriate.

More information:

In the Google Search Central Blog, Google has defined the statement about hreflang="x-default" a little more concretely. So far, I had given the English version of the blog post as the x-default page. For the simple reason - English is spoken by more people than German. In a blog post from ahrefs.com - see above - the English version was also given in an x-default example.

The x-default page can be a language and country selector page, the page you redirect users to if you don't have content for their region, or simply the version of content you consider to be the default.

In principle, I didn’t need to change anything. However, I decided to use the language and country selection page and changed the source code accordingly. More on this below.

Fortunately, I have a free account at ahrefs.com. They do a complete scan of my website once a week. My health score has dropped from 98% to 70%. Changing the x-default in the alternate link has caused this. The following error is displayed: Missing reciprocal hreflang (no return tag).

When programming, it is always good to consider the conditions you have quoted yourself. I did not observe the condition that a web page must always contain a “return link” to the calling page with the “en/lang-selector/”. Since this is the case on all pages (German and English), I now have 130 pages with really hard errors. Due to many changes that are not so obvious, starting with doFollow links, I slipped in the Google index. So that I don’t slip even further in the Google index now, I am changing the source code for Part 3 back to the respective English website at short notice.

I don’t show this in the blog post now, because I’m working on a solution. The change in the text will be made after the problem has been solved. 🧐 If you don’t have problems, you create them.

Hreflang is a simple HTML attribute, but it can be hard to grab. Google's John Mueller described hreflang as one of the most complex aspects of SEO because it gets very difficult very quickly.

Ultimately a tempest in a teapot. This blog deals with topics that only interest web developers. And that mainly with the restriction to the static website generator Hugo. If someone lands on this website via search engines, he is a professional or a prospective professional. Professionals know the website translators. If they want to use them, they will do it themselves. 80% of my visitors are not from German-speaking countries, but from all over the world.

Do I really want to show professionals who land at my site via search engines a language selection page or a link to the Google website translator first instead of the expected technical information? How would I react? I would probably close the browser window and continue my search elsewhere.

Web page translators are technically fascinating, but not helpful for this blog. I have reverted the text of the blog post back to my original version of x-default, with reference to the English blog post.

Nevertheless, I have included the source code for the solution to my original problem below.

Testing if a self reference is present

The following link provides a free test tool for hreflang alternate links. Enter a URL, select a user agent and click the button “TEST URL”.

Or

open the source view in the browser and check in the head tag if all provided languages, including a self reference, of the current page are present.

If it weren’t for the list templates with Paginator, the source code could be built very simply. But since I have added a paginator to my blog posts on the home page and in the tag lists, the code becomes quite complicated.

For didactic reasons and maybe you don’t use a paginator, I also show the simple variant without paginator here. The links have to be created in the head tag. The head tag is with me in the themes/tekki/layouts/_default/baseof.html.

The source code looks like this:

    {{ if .IsTranslated }}
    {{ range .Translations }}
    <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
    {{ end }}
    <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
    {{ end }}

Since I provide my web pages in German and English, in the {{ range .Translation }} always the other language is found. After that, the range is left and the language of the current page is output in the hreflang. This creates the so-called self-reference. The range works of course also with more than 2 languages of a website. In this version I have not included the hreflang="x-default" variant.

With Paginator Pages, .Permalink unfortunately does not return the page number. This makes the effort to enable this manually a bit more complicated. If you don’t do this effort, all Paginator Pages will have the same URL for multilingual alternate links.

The multilingual alternate links must be created after setting up the paginators. Otherwise, the respective page number cannot be accessed. I have explained the paginator source code in the post - Hugo - Pagination for blog posts and tags . Creating the URL page number of the canonical link in the post - Hugo - Canonical Pagination Links for Blog Posts and Tags .

The following source code also includes creating the paginators. I do not provide the explanation for this at this point.

    {{ $paginator := slice }}
    {{ if .IsHome }}
      {{ $paginator = .Paginate (where .Site.RegularPages.ByLastmod.Reverse "Section" "blog") }}
    {{ else }}
      {{ $paginator = .Paginate .Pages.ByLastmod.Reverse }}
    {{ end }}

{{/* Part 4 - $href for canonical link of the current web page */}}
    {{ $href := .Permalink }}
    {{ with $paginator }}
      {{ if gt .PageNumber 1 }}
        {{ $href = printf "%s%s/%d/" $.Permalink "page" .PageNumber }}
      {{ end }}
    {{ end }}
    <link rel="canonical" href="{{ $href }}">

{{/* Part 1 - No self-referencing web page */}}
    {{ if .IsTranslated }}
      {{ range .Translations }}
        {{ $lang := .Language.Lang }}
        {{ $href := .Permalink }}
        {{ with $paginator }}
          {{ if gt .PageNumber 1 }}
            {{ $href = printf "%s%s/%d/" $href "page" .PageNumber }}
            <link rel="alternate" hreflang="{{ $lang }}" href="{{ $href }}">
          {{ else }}
            <link rel="alternate" hreflang="{{ $lang }}" href="{{ $href }}">
          {{ end }}
        {{ else }}
          <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
        {{ end }}
      {{ end }}
    {{ end }}

{{/* Part 2 - Self-referencing web page */}}
    {{ $lang := .Language.Lang }}
    {{ with $paginator }}
        <link rel="alternate" hreflang="{{ $lang }}" href="{{ $href }}">
    {{ else }}
      <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
    {{ end }}

{{/* Part 3 - x-default web page */}}
    {{ if and ( ne .Page.Lang "en" ) .IsTranslated }}
      {{ range .Translations }}
          {{ $translation := . }}
          {{ if eq $translation.Lang "en"}}
            {{ $href := .Permalink }}
            {{ with $paginator }}
              {{ if gt .PageNumber 1 }}
                {{ $href = printf "%s%s/%d/" $href "page" .PageNumber }}
                <link rel="alternate" hreflang="x-default" href="{{ $href }}">
              {{ else }}
                <link rel="alternate" hreflang="x-default" href="{{ $href }}">
              {{ end }}
            {{ else }}
              <link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
            {{ end }}
          {{ end }}
      {{ end }}
    {{ else}}
      {{ $href := .Permalink }}
      {{ with $paginator }}
        {{ if gt .PageNumber 1 }}
          {{ $href = printf "%s%s/%d/" $href "page" .PageNumber }}
          <link rel="alternate" hreflang="x-default" href="{{ $href }}">
        {{ else }}
          <link rel="alternate" hreflang="x-default" href="{{ $href }}">
        {{ end }}
      {{ else }}
        <link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
      {{ end }}
    {{ end }}

For easier explanation, I have included four go comments in each section. Part 4 is only important for the content of the variable $href in Part 2. This saves me from having to create the page number links once.

  • Part 1 - creates all non-self-referencing links.
  • Part 2 - is responsible for the self-reference of the current web page.
  • Part 3 - creates the hreflang="x-default" link.
  • Part 4 - variable $href for the canonical link of the current web page.

Part 2 - self-reference of the current web page

{{/* Part 2 - Self-referencing web page */}}
    {{ $lang := .Language.Lang }}
    {{ with $paginator }}
        <link rel="alternate" hreflang="{{ $lang }}" href="{{ $href }}">
    {{ else }}
      <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
    {{ end }}

Since Part 2 is simpler, I will explain it first. This part is necessary for the so-called self-reference. Whenever a {{ with $paginator }} is queried in the source code, the instructions refer to list templates. In my case, this means the home page with the post teasers and the tag lists.

Inside the {{ with $paginator }} cannot be accessed {{ .Language.Lang }}. Otherwise, Hugo produces errors. The scope is simply not right. Hence the assignment of the language (of the current web page) to the variable $lang. For the second parameter $href, .Permalink cannot be accessed either. The content of $href is still from Part 4. In Part 1 and 3, $href is refilled with its own scope. For this reason, this section is still quite clear.

In the else of the {{ with $paginator }} which is outside of the Paginator scope, {{ .Language.Lang }} and {{ .Permalink } can be accessed normally.

{{/* Part 1 - No self-referencing web page */}}
    {{ if .IsTranslated }}
      {{ range .Translations }}
        {{ $lang := .Language.Lang }}
        {{ $href := .Permalink }}
        {{ with $paginator }}
          {{ if gt .PageNumber 1 }}
            {{ $href = printf "%s%s/%d/" $href "page" .PageNumber }}
            <link rel="alternate" hreflang="{{ $lang }}" href="{{ $href }}">
          {{ else }}
            <link rel="alternate" hreflang="{{ $lang }}" href="{{ $href }}">
          {{ end }}
        {{ else }}
          <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
        {{ end }}
      {{ end }}
    {{ end }}

If translations into other languages are available for the current web page - {{ if .IsTranslated }} then these are processed in the {{ range .Translation }}.

So that within {{ with $paginator }} can be accessed the language and the permalink yet to be extended (because of the missing scope), this data is cached in variables.

Hugo does not assign a page number to the first web page in a Paginator list. For this reason, no page number may be assigned to page 1 in the else branch of {{ if gt .PageNumber 1 }}.

If the page number is greater than 1, the page number is added to the $href variable.

The else branch of {{ with $paginator }} is responsible for all translated NOT Paginator web pages. Since outside the $paginator scope, .Language.Lang and .Permalink can be accessed again in this case.

Unfortunately, this link is even more complex to create. If no other language variant is suitable, the default or fallback page determines in which language the page is displayed to the users. Since English is more understood than German on our planet, I want to display the English variant of the web page.

{{/* Part 3 - x-default web page */}}
    {{ if and ( ne .Page.Lang "en" ) .IsTranslated }}
      {{ range .Translations }}
          {{ $translation := . }}
          {{ if eq $translation.Lang "en"}}
            {{ $href := .Permalink }}
            {{ with $paginator }}
              {{ if gt .PageNumber 1 }}
                {{ $href = printf "%s%s/%d/" $href "page" .PageNumber }}
                <link rel="alternate" hreflang="x-default" href="{{ $href }}">
              {{ else }}
                <link rel="alternate" hreflang="x-default" href="{{ $href }}">
              {{ end }}
            {{ else }}
              <link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
            {{ end }}
          {{ end }}
      {{ end }}
    {{ else}}
      {{ $href := .Permalink }}
      {{ with $paginator }}
        {{ if gt .PageNumber 1 }}
          {{ $href = printf "%s%s/%d/" $href "page" .PageNumber }}
          <link rel="alternate" hreflang="x-default" href="{{ $href }}">
        {{ else }}
          <link rel="alternate" hreflang="x-default" href="{{ $href }}">
        {{ end }}
      {{ else }}
        <link rel="alternate" hreflang="x-default" href="{{ .Permalink }}">
      {{ end }}
    {{ end }}

With the condition {{ if and ( ne .Page.Lang "en" ) .IsTranslated }} I determine whether the language of the current web page is German and has been translated. The else branch is responsible for English.

If the language of the current web page is German, I query the other languages with {{ range .Translations }}. In my case this is only English.

With {{ if eq $translation.Lang "en" }} I determine if the language of the web page is English. After that, the paginator part from above repeats with the difference that will be hreflang="x-default".

My NOT used solution for the x-default problem

The problem with the language and country selection page en/lang-selector/ was that not all “return links” to the calling web pages were set.

{{/* Part 3 - x-default web page */}}
    {{ $u := urls.Parse .Permalink }}
    {{ if eq $u.RequestURI "/en/lang-selector/" }}
      {{ range .Site.AllPages }}
        {{ if ne .Params.sitemap_exclude true }}
          {{ $u := urls.Parse .Permalink }}
          {{ if ne $u.RequestURI "/en/lang-selector/" }}
            {{ $u := urls.Parse .Permalink }}
            {{ if ne $u.RequestURI "/lang-selector/" }}
              <link rel="alternate" hreflang="{{ .Language.Lang }}" href="{{ .Permalink }}">
            {{ end }}
          {{ end }}
        {{ end }}
      {{ end }}
    {{ else if ne $u.RequestURI "/lang-selector/" }}
      {{ $href = printf "%s%s" (absURL "") "en/lang-selector/" }}
      <link rel="alternate" hreflang="x-default" href="{{ $href }}">
    {{ end }}

With - urls.Parse - I create a URL structure. Then I query with {{ if eq $u.RequestURI "/en/lang-selector/" }} whether the currently edited web page in the baseof.html is the page /en/lang-selector/.

If so, I exclude pages I don’t want displayed with {{ if ne .Params.sitemap_exclude true }}. {{ if ne $u.RequestURI "/en/lang-selector/" }} prevents duplicate entry on this page, as this self-reference has already been done by Part 2. {{ if ne $u.RequestURI "/lang-selector/" }} excludes the German version of the page, as it is not referenced anywhere and the reference was already done in Part 1. All other pages should receive a reference to /en/lang-selector/.

If no and the current page is not the German version of /lang-selector/, a “return link” is set to /en/lang-selector/ for all other web pages. The result looks like this:

Return link in /en/lang-selector/
Return links on the /en/lang-selector/ webpage to the calling pages.

At this point I realised that the language and country selection page is not a good option for my blog. What is still missing for this version is the x-default for the Paginator pages above pageNumber 1. There is no other option than to use parts of Part 3 for these pages. The link for these pages can only be created to the English page as x-default. This means that no “return link” is needed for the /en/lang-selector/ page.

Conclusion

One quickly forgets the self-reference. It happened the same way to me. The x-default website is not a must, but should be displayed according to - Google . The whole thing is complicated by the Paginator web pages. The fact that this works at all must be credited to Hugo. It is easy to forget that Hugo is a static page generator. At the moment the web pages are generated, everything must be available. With other programming languages, you can still manipulate the output HTML code during runtime and thus react to changes.

Link list to this post

This might also interest you

Update:  |
14 minutes to read
0
This post was created with Hugo version 0.115.2.

With the German language setting, comments are not displayed in the English version of the website and vice versa.

© 2023 - Frank Kunert  -  About me