×
Startseite RSS-Feed Info
Suchmaschinen die lokalisierten Versionen der eigenen Webseite bekannt machen.

SEO - Selbstreferenzierender hreflang auf mehrsprachiger Website

SEO - Selbstreferenzierender hreflang auf mehrsprachiger Website

Auf multilingualen Webseiten müssen im Header alle Sprachversionen, einschließlich einer Selbstreferenz und wenn möglich eine x-default Referenz für eine gute SEO angegeben werden. Den nötigen Hugo Sourcecode dafür erkläre ich in diesem Beitrag. Mit link rel=“alternate” hreflang="" href="" gelingt dies.

Wichtige Punkte, die bei der Nutzung von alternate Links beachtet werden müssen:

  • Jede Seite die alternate Links nutzt, benötigt einen Rücklink auf sich selbst mit der entsprechenden Sprache.
  • Jede Seite, auf welche mit einer Sprache verwiesen wird, muss auf die verweisenden Seite mit der entsprechenden Sprache zurückverweisen (A -> B, B -> A).
  • Alternative Versionen einer Seite mit anderen Sprachen ausweisen.
  • Eine x-default Seite als Standard- oder Fallbackseite ausweisen die Nutzern angezeigt wird, wenn keine andere Sprachvariante passend ist.

Weitere Informationen:

Im Google Search Central Blog hat Google die Aussage über hreflang="x-default" etwas konkreter definiert. Bisher hatte ich als x-default Seite die englische Version des Blogbeitrags angegeben. Aus dem einfachen Grund - Englisch wird von mehr Menschen gesprochen als Deutsch. In einem Blogbeitrag von ahrefs.com - siehe oben - wurde in einem x-default Beispiel auch die englische Variante angegeben.

Die x-default-Seite kann eine Sprach- und Länderauswahlseite sein, die Seite, auf die Sie Benutzer umleiten, wenn Sie keinen Inhalt für ihre Region haben, oder einfach die Version des Inhalts, die Sie als Standard betrachten.

Im Prinzip hätte ich nichts ändern müssen. Ich habe mich aber für die Sprach- und Länderauswahlseite entschieden und den Quellcode entsprechend geändert. Mehr dazu weiter unten.

Zum Glück habe ich ein kostenloses Konto bei ahrefs.com. Dort wird einmal pro Woche ein kompletter Scan meiner Website durchgeführt. Mein Health Score ist von 98% auf 70% gefallen. Die Änderung des x-default im alternate link hat dies verursacht. Der folgende Fehler wird angezeigt: Missing reciprocal hreflang (no return tag)

Beim Programmieren ist es immer gut, die Bedingungen zu berücksichtigen, die man selbst zitiert hat. Die Bedingung, dass eine Webseite immer einen “Return-Link” zur aufrufenden Seite enthalten muss, habe ich beim “en/lang-selector/” nicht eingehalten. Da dies auf allen Seiten (deutsch und englisch) der Fall ist, habe ich jetzt 130 Seiten mit wirklich harten Fehlern. Durch viele Änderungen, die nicht so offensichtlich sind, angefangen bei doFollow Links, bin ich im Google-Index abgerutscht. Damit ich jetzt im Google-Index nicht noch weiter abrutsche, ändere ich kurzfristig den Sourcecode für Part 3 wieder auf die jeweilige englische Webseite.

Dies zeige ich jetzt im Blogbeitrag nicht an, da ich an einer Lösung arbeite. Die Änderung im Text erfolgt nach Lösung des Problems. 🧐 Wer keine Probleme hat, schafft sich eben welche.

Hreflang ist ein einfaches HTML-Attribut, aber es kann schwer zu greifen sein. Googles John Mueller beschrieb hreflang als einen der komplexesten Aspekte von SEO weil es sehr schnell sehr schwierig wird.

Letztendlich ein Sturm im Wasserglas. Dieser Blog behandelt Themen, die nur Webentwickler*innen interessieren. Und das hauptsächlich mit der Beschränkung auf den statischen Webseiten-Generator Hugo. Wenn jemand über Suchmaschinen auf dieser Website landet, ist er ein Profi oder ein angehender Profi. Profis kennen die Webseiten-Übersetzer. Wenn sie sie benutzen wollen, werden sie es selbst tun. 80% meiner Besucher kommen nicht aus dem deutschsprachigen Raum, sondern aus der ganzen Welt.

Will ich Profis, die über Suchmaschinen bei mir landen, statt der erwarteten technischen Informationen wirklich erst einmal eine Sprachauswahlseite oder einen Link zum Google-Webseitenübersetzer anzeigen? Wie würde ich reagieren? Wahrscheinlich würde ich das Browserfenster schließen und woanders weitersuchen.

Webseitenübersetzer sind technisch faszinierend, aber für diesen Blog nicht hilfreich. Ich habe den Text des Blogbeitrags wieder auf meine ursprüngliche Version von x-default zurückgesetzt, mit Verweis auf den englischen Blogbeitrag.

Dennoch habe ich den Quellcode für die Lösung meines ursprünglichen Problems unten angefügt.

Testen ob eine Selbstreferenz vorhanden ist

Der folgende Link stellt ein kostenloses Test Tool für hreflang alternate Links zur Verfügung. Eine URL eintragen, einen User Agent wählen und den Button “TEST URL” klicken.

Oder

im Browser die Source Ansicht öffnen und im head Tag nachschauen ob alle zur Verfügung gestellten Sprachen, einschließlich einer Selbstreferenz der aktuellen Seite vorhanden ist.

Wenn die List-Templates mit Paginator nicht wären könnte der Sourcecode sehr einfach aufgebaut werden. Da ich aber meine Blogbeiträge auf der Startseite und in den Tag-Listen mit einem Paginator versehen habe wird der Code doch ganz schön kompliziert.

Aus didaktischen Gründen und vielleicht benutzen Sie ja keinen Paginator, zeige ich hier auch die einfache Variante ohne Paginator. Die Links müssen im head Tag erstellt werden. Das head Tag ist bei bei mir in der themes/tekki/layouts/_default/baseof.html.

Der Sourcecode sieht wie folgt aus:

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

Da ich meine Webseiten in deutsch und englisch zur Verfügung stelle, wird im {{ range .Translation }} immer die jeweils andere Sprache gefunden. Danach wird der range verlassen und die Sprache der aktuellen Seite im hreflang ausgegeben. Dadurch wird die sogenannte Selbstreferenz erstellt. Der range funktioniert natürlich auch bei mehr als 2 Sprachen einer Website. In dieser Version habe ich die hreflang="x-default" Variante nicht berücksichtigt.

Bei Paginator Pages gibt .Permalink leider nicht die Seitennummer zurück. Dadurch wird der Aufwand dies manuell zu ermöglichen etwas komplizierter. Wenn man diesen Aufwand nicht betreibt, haben alle Paginator Pages bei multilingualen alternate Links die gleiche URL.

Die multilingualen alternate Links müssen nach dem Einrichten der Paginatoren erstellt werden. Ansonsten kann nicht auf die jeweilige Seitennummer zugegriffen werden. Den Paginator Sourcecode habe ich in dem Beitrag - Hugo - Pagination für Blogbeiträge und Tags - erklärt. Die Erstellung der URL Seitennummer des canonical Links im Beitrag - Hugo - Canonical Pagination Links für Blogbeiträge und Tags .

Der folgende Sourcecode beinhaltet auch das Erstellen der Paginatoren. Die Erklärung dazu gebe ich an dieser Stelle nicht.

    {{ $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 }}

Zur einfacheren Erklärung habe ich vier go-Kommentare in den einzelnen Abschnitten eingefügt. Part 4 ist nur für den Inhalt der Variablen $href im Part 2 wichtig. Dadurch kann ich mir das Erstellen der Seitennummer Links einmal ersparen.

  • Part 1 - erstellt alle NICHT selbstreferenzierenden Links.
  • Part 2 - ist für die Selbstreferenz der aktuellen Webseite zuständig.
  • Part 3 - erstellt den hreflang="x-default" Link.
  • Part 4 - Variable $href für den canonical Link der aktuellen Webseite.

Part 2 - Selbstreferenz der aktuellen Webseite

{{/* 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 }}

Da der Part 2 einfacher aufgebaut ist, erkläre ich den zuerst. Dieser Teil ist für die sogenannte Selbstreferenz nötig. Immer wenn ein {{ with $paginator }} im Sourcecode abgefragt wird, beziehen sich die Anweisungen auf List-Templates. In meinem Fall sind damit die Startseite mit den Beitragsteasern und die Tag-Listen gemeint.

Innerhalb des {{ with $paginator }} kann nicht auf {{ .Language.Lang }} zugegriffen werden. Ansonsten produziert Hugo Fehler. Der Scope stimmt einfach nicht. Deshalb die Zuweisung der Sprache (der aktuellen Webseite) an die Variable $lang. Beim zweiten Parameter $href kann ebenfalls nicht auf .Permalink zugegriffen werden. Der Inhalt von $href stammt noch aus Part 4. Im Part 1 und 3 wird $href mit einem eigenen Scope neu gefüllt. Aus diesem Grund ist dieser Abschnitt noch recht übersichtlich.

Im else vom {{ with $paginator }} welcher außerhalb des Paginator Scopes liegt, kann ganz normal auf {{ .Language.Lang }} und {{ .Permalink }} zugegriffen werden.

{{/* 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 }}

Wenn für die aktuelle Webseite Übersetzungen in andere Sprachen vorliegen - {{ if .IsTranslated }} dann werden diese im {{ range .Translation }} abgearbeitet.

Damit innerhalb von {{ with $paginator }} auf die Sprache und den noch zu erweiternden Permalink (wegen dem fehlenden Scope) zugegriffen werden kann, werden diese Daten in Variablen zwischengespeichert.

Der ersten Webseite einer Paginator Liste weißt Hugo keine Seitennummer zu. Aus diesem Grund darf der Seite 1 im else Zweig von {{ if gt .PageNumber 1 }} keine Seitennummer zugewiesen werden.

Wenn die Seitennummer größer als 1 ist, wird die Variable $href mit der Seitennummer ergänzt.

Der else Zweig von {{ with $paginator }} ist für alle übersetzten NICHT Paginator Webseiten zuständig. Da außerhalb des $paginator Scopes, kann in diesem Fall wieder auf .Language.Lang und .Permalink zugegriffen werden.

Dieser Link ist leider noch aufwändiger zu erstellen. Wenn keine andere Sprachvariante passend ist, legt die Standard- oder Fallbackseite fest in welcher Sprache die Seite den Nutzern angezeigt wird. Da englisch eher als deutsch auf unserem Planeten verstanden wird, möchte ich die englische Variante der Webseite anzeigen.

{{/* 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 }}

Mit der Bedingung {{ if and ( ne .Page.Lang "en" ) .IsTranslated }} stelle ich fest, ob die Sprache der aktuellen Webseite deutsch ist und übersetzt wurde. Der else Zweig ist für englisch zuständig.

Wenn die Sprache der aktuellen Webseite deutsch ist, frage ich mit {{ range .Translations }} die anderen Sprachen ab. In meinem Fall ist dies nur englisch.

Mit {{ if eq $translation.Lang "en" }} stelle ich fest ob die Sprache der Webseite englisch ist. Danach wiederholt sich der Paginator Teil von oben mit dem Unterschied, dass hreflang="x-default" ist.

Meine NICHT benutzte Lösung für das x-default Problem

Das Problem der Sprach- und Länderauswahlseite en/lang-selector/ bestand darin, dass nicht alle “Return-Links” zu den aufrufenden Webseiten gesetzt wurden.

{{/* 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 }}

Mit - urls.Parse - erstelle ich eine URL-Struktur. Dann frage ich mit {{ if eq $u.RequestURI "/en/lang-selector/" }} ab, ob die aktuell bearbeitete Webseite in der baseof.html die Seite /en/lang-selector/ ist.

Wenn ja, schließe ich mit {{ if ne .Params.sitemap_exclude true }} Seiten aus, die ich nicht angezeigt haben möchte. {{ if ne $u.RequestURI "/en/lang-selector/" }} verhindert eine doppelte Eingabe auf dieser Seite, da diese Selbstreferenz bereits durch Part 2 erfolgt ist. {{ if ne $u.RequestURI "/lang-selector/" }} schließt die deutsche Version der Seite aus, da sie nirgendwo referenziert wird und der Verweis schon in Part 1 durchgeführt wurde. Alle anderen Seiten sollen einen Verweis auf /en/lang-selector/ erhalten.

Wenn nein und die aktuelle Seite ist nicht die deutsche Version von /lang-selector/ wird für alle anderen Webseiten ein “Return-Link” auf /en/lang-selector/ gesetzt. Das Ergebnis sieht wie folgt aus:

Return-Link im /en/lang-selector/
Return-Links auf der Webseite /en/lang-selector/ zu den aufrufenden Seiten.

An dieser Stelle ist mir bewußt geworden, dass die Sprach- und Länderauswahlseite keine gute Option für meinen Blog ist. Was für diese Version noch fehlt ist der x-default für die Paginator Seiten oberhalb der pageNumber 1. Es gibt keine andere Möglichkeit als für diese Seiten Teile von Part 3 zu benutzen. Der Verweis für diese Seiten kann nur auf die englische Seite als x-default erstellt werden. Dadurch wird kein “Return-Link” für die /en/lang-selector/ Seite benötigt.

Fazit

Die Selbstreferenz vergisst man schnell. Ist mir auch so gegangen. Die x-default Webseite ist kein muss, sollte aber laut - Google - angezeigt werden. Kompliziert wird die ganze Sache durch die Paginator Webseiten. Das dies überhaupt so funktioniert muss man Hugo hoch anrechnen. Man vergisst leicht, das Hugo ein statischer Seitengenerator ist. In dem Moment wo die Webseiten generiert werden muss alles zur Verfügung stehen. Bei anderen Programmiersprachen kann man während der Laufzeit immer noch den ausgegebenen HTML-Code manipulieren und dadurch auf Änderungen reagieren.

Linkliste zu diesem Beitrag

Das könnte Sie auch interessieren

Update:  |
14 Minuten Lesezeit
0
Dieser Beitrag wurde mit der Hugo-Version 0.112.3 erstellt.

Kommentare werden bei deutscher Spracheinstellung nicht in der englischen Variante der Webseite angezeigt und umgekehrt.

© 2023 - Frank Kunert  -  Ich über mich