├Ś
Startseite RSS-Feed Info
Schema.org Typ WebSite und BlogPosting - strukturierte Daten f├╝r eine bessere SEO meiner Hugo Website.

Hugo - Strukturierte Schema Daten f├╝r eine bessere SEO

Hugo - Strukturierte Schema Daten f├╝r eine bessere SEO

Strukturierte Daten erm├Âglichen Suchmaschinen die Daten auf der Website besser zu verstehen und dadurch die Suchergebnisse f├╝r die Nutzer ansprechender darzustellen. Dieser Beitrag erkl├Ąrt mein Hugo Partial f├╝r die Schema.org Typen WebSite und BlogPosting. Mit i18n Eintr├Ągen, Erkl├Ąrung des Sourcecodes und Testen der JSON-LD Struktur.

Beitrag aktualisiert am 03.06.2023

Ich habe diesen Beitrag etwas überarbeitet und an die Hugo-Änderungen der Version 0.112 angepasst.

Allgemeine Informationen zu strukturierten Daten

Google stellt f├╝r Entwickler gute Informationen zur Verf├╝gung. Nachfolgend einige Links zum Thema:

Die von Google, Microsoft, Yahoo und Yandex gegr├╝ndeten Schema.org-Vokabularien f├╝r strukturierte Daten werden in einem offenen Gemeinschaftsprozess entwickelt und von den Suchmaschinen entsprechend ber├╝cksichtigt.

Bedanken m├Âchte ich mich bei Ashish Lahoti. Sein Blogbeitrag - Add Structure Data JSON-LD in Hugo Website Pages - hat mir beim programmieren meines Partial sehr geholfen.

Relevante hugo.toml Eintr├Ąge

Nachfolgend die im Schema benutzten Eintr├Ąge in der Konfigurationsdatei von Hugo:

[author]
  name = "Frank Kunert"
  license = "Frank Kunert"
..
[languages]
  [languages.de]
    languageName = "Deutsch"
    weight = 1
    title = "Hugo, Webdev, SEO, Tools"
    [languages.de.params]
      description = "Blog ├╝ber Hugo, Webdev, SEO, Tools"
      mydomain = "tekki-tipps.de ­čçę­čç¬"
    ..
  [languages.en]
    languageName = "English"
    weight = 2
    title = "Hugo, Webdev, SEO, Tools"
    [languages.en.params]
      description = "Blog about Hugo, Webdev, SEO, Tools"
      mydomain = "tekki-tipps.de/en/ ­čçČ­čçž"
    ..

Die Eintr├Ąge in der hugo.toml wurden von mir nachtr├Ąglich an die Hugo-Version 0.112 angepasst.

Relevante Front Matter Eintr├Ąge

Folgende Front Matter Eintr├Ąge werden im Partial site-schema.html verwendet:

title: "Hugo - Strukturierte Schema Daten f├╝r eine bessere SEO"
description: "Hugo Partial f├╝r die Schema.org Typen WebSite und Blog mit i18n Eintr├Ągen. Erkl├Ąrung des Sourcecodes und Testen der JSON-LD Struktur. F├╝r eine bessere SEO."
subtitle: "Schema.org Typ WebSite und Blog - strukturierte Daten f├╝r eine bessere SEO meiner Hugo Website."
author: "Frank Kunert"

Partial site-schema.html

Das Partial wird im head Tag der baseof.html eingebunden:

<!DOCTYPE html>
<html lang="{{- .Site.Language.Lang -}}">
  <head>
    <meta charset="UTF-8">
    ...
    ...
    {{ partial "site-schema.html" . }}
  </head>
  <body>
  </body>
</html>

Ein Partial f├╝r verschiedene Bereiche der Website:

  • Startseite inklusive der Pagination Seiten - page/2, page/3 …
  • Tag-Listen inklusive der Pagination Seiten - page/2, page/3 …
  • Blog Webseiten

Der Sourcecode von themes/tekki/layouts/partials/site-schema.html sieht wie folgt aus:

{{ if or ( .IsHome ) ( not .Description ) -}}
  <script type="application/ld+json">
  {
    "@context": "http://schema.org",
    "@type": "WebSite",
    "name": "{{ .Site.Params.mydomain }}",
    "url": "{{ printf "%s%s" .Site.BaseURL (substr .Paginator.URL 1) }}",
    {{ if .IsHome }}
      "description": "{{ .Site.Params.description }}",
    {{ else }}
      {{ if eq .Site.Language.Lang "de" }}
        {{ $u := urls.Parse .Permalink }}
        {{ $u := strings.TrimRight "/" $u.Path }}
        {{ $u := substr $u 6 }}
        "description": "Tag {{ $u }} - {{ .Site.Params.description }}",
      {{ else }}
        {{ $u := urls.Parse .Permalink }}
        {{ $u := strings.TrimRight "/" $u.Path }}
        {{ $u := substr $u 9 }}
        "description": "Tag {{ $u }} - {{ .Site.Params.description }}",
      {{ end }}
    {{ end }}
    "inLanguage": {{ if eq .Site.Language.Lang "de" }}{{"de"}}{{ else }}{{"en"}}{{ end }},
    "thumbnailUrl": "{{ "/icon-144.png" | absURL }}",
    "image": {
      "@type": "ImageObject",
      "@id": "{{ "/tekki-tipps-og.png" | absURL }}",
      "url": "{{ "/tekki-tipps-og.png" | absURL }}",
      "height": "630",
      "width": "1200"
    },
    "license": "{{ .Site.Author.license }}"
  }
  </script>
{{ else if .IsPage }}
  {{ if eq .Section "blog" }}
    {{ $author :=  or (.Params.author) (.Site.Author.name) }}
    {{ $org_name :=  .Site.Params.mydomain }}
    <script type="application/ld+json">
    {
      "@context": "http://schema.org",
      "@type": "BlogPosting",
      "articleSection": "{{ .Section }}",
      "name": "{{ .Title | safeJS }}",
      "headline": "{{ .Title | safeJS }}",
      "alternativeHeadline": "{{ .Params.subtitle }}",
      "description": "{{if .IsPage}}{{ .Summary | markdownify }}{{ else }}{{ if .Description }}{{ .Description | safeJS }}{{ end }}{{ end }}",
      "inLanguage": {{ if eq .Site.Language.Lang "de" }}{{"de"}}{{ else }}{{"en"}}{{ end }},
      "isFamilyFriendly": "true",
      "mainEntityOfPage": {
        "@type": "WebPage",
        "@id": "{{ .Permalink }}"
      },
      "author" : {
        "@type": "Person",
        "name": "{{ $author }}",
        {{ with .Site.GetPage "/ueber-mich" -}}"url": "{{- .Permalink -}}"{{ end }}
      },
      "creator" : {
      "@type": "Person",
        "name": "{{ $author }}"
      },
      "accountablePerson" : {
        "@type": "Person",
        "name": "{{ $author }}"
      },
      "copyrightHolder": "{{ $org_name }}",
      "copyrightYear": "{{ .Date.Format "2006" }}",
      "dateCreated": "{{ .Date.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
      "datePublished": "{{ .PublishDate.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
      "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
      "publisher":{
        "@type":"Organization",
        "name": {{ $org_name }},
        "url": {{ .Permalink }},
        "logo": {
          "@type": "ImageObject",
          "url": "{{ "/icon-144.png" | absURL }}",
          "width": "144",
          "height": "144"
        }
      },
      {{ with .Resources.GetMatch "article-img" }}{{ $banner := $.Page.Resources.GetMatch "article-img" }}
        "image": {
          "@type": "ImageObject",
          "@id": "{{ $banner.Permalink }}",
          "url": "{{ $banner.Permalink }}",
          "height": "{{ $banner.Height }}",
          "width": "{{ $banner.Width}}"
        },
      {{ end }}
      "url": "{{ .Permalink }}",
      "wordCount": "{{ .WordCount }}",
      "genre": [ {{ range $index, $tag := .Params.tags }}{{ if $index }}, {{ end }}"{{ $tag }}" {{ end }}]
    }
    </script>
  {{ end }}
{{ end }}

Weiter unten teile ich den Sourcecode nach den einzelnen Bereichen auf und erkl├Ąre einige Dinge.

Startseite und Tag-Listen inkl. der Pagination Seiten - page/2 und folgende

{{ if or ( .IsHome ) ( not .Description ) -}}
  <script type="application/ld+json">
  {
    "@context": "http://schema.org",
    "@type": "WebSite",
    "name": "{{ .Site.Params.mydomain }}",
    "url": "{{ printf "%s%s" .Site.BaseURL (substr .Paginator.URL 1) }}",
    {{ if .IsHome }}
      "description": "{{ .Site.Params.description }}",
    {{ else }}
      {{/* Tag-Listen */}}
      {{ if eq .Site.Language.Lang "de" }}
        {{ $u := urls.Parse .Permalink }}
        {{ $u := strings.TrimRight "/" $u.Path }}
        {{ $u := substr $u 6 }}
        "description": "Tag {{ $u }} - {{ .Site.Params.description }}",
      {{ else }}
        {{ $u := urls.Parse .Permalink }}
        {{ $u := strings.TrimRight "/" $u.Path }}
        {{ $u := substr $u 9 }}
        "description": "Tag {{ $u }} - {{ .Site.Params.description }}",
      {{ end }}
    {{ end }}
    "inLanguage": {{ if eq .Site.Language.Lang "de" }}{{"de"}}{{ else }}{{"en"}}{{ end }},
    "thumbnailUrl": "{{ "/icon-144.png" | absURL }}",
    "image": {
      "@type": "ImageObject",
      "@id": "{{ "/tekki-tipps-og.png" | absURL }}",
      "url": "{{ "/tekki-tipps-og.png" | absURL }}",
      "height": "630",
      "width": "1200"
    },
    "license": "{{ .Site.Author.license }}"
  }
  </script>
  ..

In diesem Sourcecode Abschnitt ist auch der Code f├╝r die Tag-Listen enthalten. Der einzige Unterschied ist das Erstellen der "description". Aber eins nach dem anderen.

Mit {{ if or ( .IsHome ) ( not .Description ) -}} frage ich ab, ob die zu generierende Seite die Startseite oder Pagination Folgeseite der Startseite ist. Oder ob die Seite keine ( not .Description ) hat. Die Startseite hat auch keine Description, wird aber ├╝ber .IsHome erkannt. Die Tag-Listen sind die einzigen Seiten auf meiner Website die ebenfalls keine Description haben. Mit diesem Trick kann ich auf die Tag-Listen zugreifen. Dieses Partial ist ja in der baseof.html eingebunden, also zu einem ganz fr├╝hen Zeitpunkt. Nur dort kann auf das head HTML-Tag zugegriffen und die json-Datei erstellt werden.

Bei der Zuweisung von "name": "{{ .Site.Params.mydomain }}", greife ich auf die hugo.toml und dort auf den Parameter mydomain zu. Je nach Sprache der aktuellen Webseite wird der Inhalt zugewiesen.

— UPDATE 03.06.2023
"url": "{{ printf “%s%s” .Site.BaseURL (substr .Paginator.URL 1) }}" weist die aktuelle URL zu, einschlie├člich der Folgeseitennummer, falls vorhanden. Die URL setzt sich aus den Komponenten .Site.BaseURL, die sich in der hugo.toml befindet, und .Paginator.URL zusammen. (substr .Paginator.URL 1) entfernt den f├╝hrenden Backslash aus der .Paginator.URL. Andernfalls w├╝rde dieser doppelt vorkommen.
— Ende UPDATE 03.06.2023

Jetzt kommen wir zur description. Mit {{ if .IsHome }} frage ich wieder ab ob die aktuelle Webseite eine Start- oder Folgeseite der Startseite ist. Wenn ja, dann weise ich die sprachspezifische Description aus der hugo.toml zu.

Wenn nein, ist die aktuelle Webseite eine Tag-Liste oder Folgeseite einer Tag-Liste. Nun wird es etwas komplizierter. Was ich machen m├Âchte ist, vor der allgemeinen Description aus der hugo.toml das Wort Tag und dann den Tag-Namen mit Bindestrich auszugeben. Dazu muss ich den Tag-Namen aus der URL herausoperieren. Da die URL bei ├╝bersetzten Seiten nach dem Muster /en/tags/<tag-name>/ aufgebaut ist muss ich dort 3 Zeichen mehr rausschneiden. Deshalb die Abfrage nach der Language. Mit Scratch h├Ątte ich noch mehr Code produziert, auch deshalb die Code-Wiederholung.

{{ $u := urls.Parse .Permalink }} der Variablen $u wird mit der Hugo Funktion - urls.Parse - der .Permalink zugewiesen. {{ $u := strings.TrimRight “/” $u.Path }} schneidet den letzten / aus dem String. Und {{ $u := substr $u 6 }} schneidet die ersten 6 Zeichen in der deutschen Version ab. In der englischen Variante sind es 9 Zeichen. Der restliche Code ist selbsterkl├Ąrend.

Test-Ergebnis f├╝r die Startseite und Tag-Liste

Die Website schema.org stellt einen - Schema Markup Validator - zur Verf├╝gung. Das Test-Ergebnis f├╝r die Startseite sieht wie folgt aus:

Schema.org Test Typ Website
Schema.org - Test der Schema JSON-LD Daten vom Typ Website f├╝r die Startseite.

F├╝r die Tag-Liste sieht das Ergebnis so aus:

Schema.org Test Typ Website Tag-List
Schema.org - Test der Schema JSON-LD Daten vom Typ Website f├╝r die Tag-Liste.

Blog Webseiten

Da ich nur meine Blog Webseiten den Suchmaschinen per Schema anbieten m├Âchte, filter ich auch nur diese Seiten durch {{ if eq .Section "blog" }} raus. Datenschutz und Impressum ben├Âtigen meiner Meinung nach kein Schema.

..
{{ else if .IsPage }}
  {{ if eq .Section "blog" }}
    {{ $author := or (.Params.author) (.Site.Author.name) }}
    {{ $org_name := .Site.Params.mydomain }}
    <script type="application/ld+json">
    {
      "@context": "http://schema.org",
      "@type": "BlogPosting",
      "articleSection": "{{ .Section }}",
      "name": "{{ .Title | safeJS }}",
      "headline": "{{ .Title | safeJS }}",
      "alternativeHeadline": "{{ .Params.subtitle }}",
      "description": "{{if .IsPage}}{{ .Summary | markdownify }}{{ else }}{{ if .Description }}{{ .Description | safeJS }}{{ end }}{{ end }}",
      "inLanguage": {{ if eq .Site.Language.Lang "de" }}{{"de"}}{{ else }}{{"en"}}{{ end }},
      "isFamilyFriendly": "true",
      "mainEntityOfPage": {
        "@type": "WebPage",
        "@id": "{{ .Permalink }}"
      },
      "author" : {
        "@type": "Person",
        "name": "{{ $author }}",
        {{ with .Site.GetPage "/ueber-mich" -}}"url": "{{- .Permalink -}}"{{ end }}
      },
      "creator" : {
      "@type": "Person",
        "name": "{{ $author }}"
      },
      "accountablePerson" : {
        "@type": "Person",
        "name": "{{ $author }}"
      },
      "copyrightHolder": "{{ $org_name }}",
      "copyrightYear": "{{ .Date.Format "2006" }}",
      "dateCreated": "{{ .Date.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
      "datePublished": "{{ .PublishDate.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
      "dateModified": "{{ .Lastmod.Format "2006-01-02T15:04:05.00Z" | safeHTML }}",
      "publisher":{
        "@type":"Organization",
        "name": {{ $org_name }},
        "url": {{ .Permalink }},
        "logo": {
          "@type": "ImageObject",
          "url": "{{ "/icon-144.png" | absURL }}",
          "width": "144",
          "height": "144"
        }
      },
      {{ with .Resources.GetMatch "article-img" }}{{ $banner := $.Page.Resources.GetMatch "article-img" }}
        "image": {
          "@type": "ImageObject",
          "@id": "{{ $banner.Permalink }}",
          "url": "{{ $banner.Permalink }}",
          "height": "{{ $banner.Height }}",
          "width": "{{ $banner.Width}}"
        },
      {{ end }}
      "url": "{{ .Permalink }}",
      "wordCount": "{{ .WordCount }}",
      "genre": [ {{ range $index, $tag := .Params.tags }}{{ if $index }}, {{ end }}"{{ $tag }}" {{ end }}]
    }
    </script>
  {{ end }}
{{ end }}

Da $author und $org_name an mehr als einer Stelle ben├Âtigt werden, habe ich daf├╝r diese Variablen mit dem entsprechenden Inhalt gef├╝llt. Jeder Blog Beitrag hat bei mir einen Subtitel. Mit "alternativeHeadline": "{{ .Params.subtitle }}" weise ich den Subtitel dem Property zu.

Der Description weise ich mit "description": "{{if .IsPage}}{{ .Summary | markdownify }}{{ else }}{{ if .Description }}{{ .Description | safeJS }}{{ end }}{{ end }}" die Summary des Blog Beitrags zu. Mit | markdownify filter ich das p HTML-Tag raus. Falls keine Summary vorhanden - kommt bei mir nicht vor - wird die Description aus dem Front Matter der aktuellen Seite entnommen.

Das Test-Tool von Google hat mich bei dem Property author darauf hingewiesen, dass eine URL zu einer Webseite ├╝ber den Autor hinzugef├╝gt werden sollte. Aus diesem Grund habe ich meine "Ich ├╝ber mich" Webseite auf dieser Site angegeben. Mit {{ with .Site.GetPage "/ueber-mich" -}}"url": "{{- .Permalink -}}"{{ end }} suche ich die Seite und f├╝ge deren Permalink hinzu. Dies funktioniert ebenfalls in der englische Version. "/ueber-mich" ist der Page Bundle Verzeichnisname meiner "Ich ├╝ber mich" Webseite.

Meinen Blog Beitr├Ągen weise ich im Page Bundle ein Bild zu. Dies hat immer den gleichen Resourcenamen. Mit {{ with .Resources.GetMatch "article-img" }}{{ $banner := $.Page.Resources.GetMatch "article-img" }} .. {{ end }} hole ich das Bild und weise es dem Property "image": zu.

Test-Ergebnis f├╝r die Blog Webseiten

Das Test-Ergebnis f├╝r die Blog Webseiten sieht so aus:

Schema.org Test Typ Blog
Schema.org - Test der Schema JSON-LD Daten vom Typ BlogPosting.

Test der JSON-LD Schema Daten

Neben dem schon weiter oben beschriebenen Schema.org Validator gibt es auch von Google einen entsprechenden Validator f├╝r Rich-Suchergebnisse. Schema.org pr├╝ft ob die Schemas eingehalten werden. Googles Test f├╝r Rich-Suchergebnisse pr├╝ft ob die zus├Ątzlichen Kriterien von Google eingehalten werden.

Auch wenn auf der eigenen Website keine oder nur wenige Rich-Results m├Âglich sind, werden den Suchmaschinen mit den Schemas eine zus├Ątzliche M├Âglichkeit zur Verf├╝gung gestellt, die Webseite besser zu verstehen. Ob sich das dann auch auf die Suchergebnis-Position auswirkt ist nicht garantiert.

Fazit

Auf den ersten Blick sieht der Partial Sourcecode etwas kompliziert aus, aber bei n├Ąherer Betrachtung ist dies gar nicht so schlimm. Die Properties f├╝r die einzelnen Schema.org Typen werden auf Schema.org ganz gut erkl├Ąrt. Erste Auswirkungen habe ich in der Google Suche schon bemerkt. Es wird jetzt teilweise ein kleines Bild neben den Sucheintr├Ągen meiner Blog Beitr├Ąge angezeigt.

Linkliste zu diesem Beitrag

Das k├Ânnte Sie auch interessieren

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

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

© 2023 - Frank Kunert  -  Ich ├╝ber mich