×
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.

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": "{{ .Permalink }}",
    {{ 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

Leider bietet Hugo sehr eingeschrĂ€nkte Möglichkeiten um die Folgeseiten-Nummer einer Pagination zu ermitteln. Ich hĂ€tte diese gerne in den Titel und Description der Folgeseiten eingebaut. Es ist aber an dieser Stelle der baseof.html nicht möglich ĂŒberhaupt zu erkennen ob die aktuelle Seite eine Folgeseite ist.

{{ if or ( .IsHome ) ( not .Description ) -}}
  <script type="application/ld+json">
  {
    "@context": "http://schema.org",
    "@type": "WebSite",
    "name": "{{ .Site.Params.mydomain }}",
    "url": "{{ .Permalink }}",
    {{ 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.

"url": "{{ .Permalink }}" weist die aktuelle URL zu. Leider kann Hugo nicht die Folgeseiten mit .Permalink ausgeben. Es wird bei der Startseite nur deren URL ausgegeben. Mir ist keine andere Go-Funktion bekannt, mit der man die URL direkt auslesen kann. Ob das zu diesem frĂŒhen Zeitpunkt der Generierung ĂŒberhaupt möglich ist, ist mir unbekannt. Wenn es einfach wĂ€re hĂ€tte das zustĂ€ndige Hugo-Team dies bestimmt schon bereitgestellt. Bei den Tag-Listen wird noch ein /tags/<tag-name>/ hinzugefĂŒgt, aber auch kein /page/<folgeseiten-nummer>.

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

|
12 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