Zum Inhalt springen

Strukturierte Daten in Next.js, aus einem Graphen erzeugt

Die meisten JSON-LD-Implementierungen liefern vier zusammenhanglose Blöcke, die jeweils die Organisation neu deklarieren. Ein Graph mit stabilen @id-Referenzen ist weniger Code - und auflösbar.

6 Min. Lesezeit

Strukturierte Daten sind der Teil des technischen SEO mit der größten Lücke zwischen dem, was Teams ausliefern, und dem, was die Spezifikation belohnt. Die übliche Umsetzung ist eine Komponente pro Seitentyp, jede mit eigenem <script type="application/ld+json">, jede mit eigener Deklaration von Organisation, Website und Logo.

Das validiert. Es sagt einem Crawler aber auch, dass Ihre Website vier unverbundene Organisationen enthält, die zufällig denselben Namen tragen.

Ein Graph statt vier Blöcke

Der Schlüssel @graph existiert genau dafür. Er enthält eine Liste von Knoten, jeder mit einer stabilen @id, und die Knoten verweisen über diese @id aufeinander, statt einander zu wiederholen.

{
  "@context": "https://schema.org",
  "@graph": [
    { "@type": "Organization", "@id": "https://example.com/#org", "name": "Example" },
    { "@type": "WebSite", "@id": "https://example.com/#site", "publisher": { "@id": "https://example.com/#org" } },
    { "@type": "WebPage", "@id": "https://example.com/blog/post#page", "isPartOf": { "@id": "https://example.com/#site" } },
    { "@type": "Article", "@id": "https://example.com/blog/post#article", "mainEntityOfPage": { "@id": "https://example.com/blog/post#page" } }
  ]
}

Vier Knoten, eine Organisation, und jede Beziehung genau einmal ausgesprochen. Die @id-Werte sind der eigentliche Mechanismus: Sie erlauben einem Crawler, den Knoten hier mit demselben Knoten auf jeder anderen Seite zu verschmelzen.

Zwei Regeln machen sie brauchbar:

  • Immer absolute URLs. #org allein ist kein Bezeichner, sondern ein Fragment, das auf jeder Seite etwas anderes bedeutet.
  • Für immer stabil. Eine @id, die sich zwischen Deployments ändert - weil sie aus einem bearbeiteten Slug oder einer Datenbank-ID gebaut wurde -, verwirft alles, was der Crawler dazu gesammelt hatte.

Erzeugen statt schreiben

Handgeschriebenes JSON-LD driftet, weil nichts es zwingt, mit der Seite übereinzustimmen. Der Titel ändert sich im metadata-Export und die headline in den strukturierten Daten behält den alten, weil es zwei Strings in zwei Dateien sind.

Die Lösung ist ein typisierter Helfer, aufgerufen dort, wo die Antwort schon bekannt ist:

// lib/jsonld.ts
export function articleNode({ url, title, description, published, modified }: ArticleInput) {
  return {
    '@type': 'Article',
    '@id': `${url}#article`,
    headline: title,
    description,
    datePublished: published,
    dateModified: modified ?? published,
    mainEntityOfPage: { '@id': `${url}#page` },
    publisher: { '@id': `${SITE}#org` },
  };
}
// app/blog/[slug]/page.tsx
<JsonLd data={graph(
  webPageNode({ url, title: doc.title, description: doc.description }),
  articleNode({ url, title: doc.title, description: doc.description,
                published: doc.date, modified: doc.updated }),
  breadcrumbNode(trail),
)} />

Jetzt können die strukturierten Daten der Seite nicht mehr widersprechen, weil sie dieselben Variablen lesen, die die Seite rendert. Das ist dasselbe Prinzip wie bei Canonicals und Sitemaps, die nicht auseinanderlaufen können: eine Quelle der Wahrheit, an beiden Enden erzeugt.

Wo es in die Antwort gehört

In das serverseitig gerenderte HTML. Ein <script>, das eine Client-Komponente nach der Hydration einfügt, ist Markup, das ein Crawler ausführen kann oder auch nicht - und es gibt keinen Grund, die Wette einzugehen, denn die Daten sind auf dem Server bekannt.

export function JsonLd({ data }: { data: object }) {
  return (
    <script
      type="application/ld+json"
      dangerouslySetInnerHTML={{ __html: JSON.stringify(data) }}
    />
  );
}

dangerouslySetInnerHTML ist hier korrekt und keine Abkürzung: React escapt Textkinder, was das JSON zerstört. Was Sie nicht tun dürfen, ist nutzergesteuerten Text ohne Escaping von < einzusetzen - ein Kommentar, der </script> enthält, schließt sonst das Tag und führt aus, was folgt.

Welche Typen sich lohnen und welche Theater sind

Strukturierte Daten verdienen ihren Platz, wenn sie auf ein Rich Result abbilden oder eine Entität auflösen. Darüber hinaus sind sie Markup, das niemand liest.

Lohnen sich:

  • Organization und WebSite, einmal, überall referenziert.
  • BreadcrumbList, das direkt in den Ergebnissen erscheint.
  • Article auf Beiträgen, Product mit echten Offer-Daten auf Produkten.
  • FAQPage, aber nur dort, wo die Fragen sichtbar auf der Seite stehen. Nicht gerenderte Fragen auszuzeichnen ist ein Richtlinienverstoß, kein Trick.
  • LocalBusiness oder ein Subtyp, wo es eine echte Adresse gibt. Die Subtypen verlangen address - ProfessionalService ohne Adresse ist ungültig, und das ist ein verbreiteter Weg, einen kaputten Graphen auszuliefern und ihn für reichhaltiger zu halten.

Lohnen sich kaum:

  • Review, das Sie über sich selbst geschrieben haben. Eigenlob-Markup ist seit Jahren nicht mehr für Rich Results zugelassen.
  • HowTo und Recipe auf Seiten, die weder das eine noch das andere sind.
  • speakable, SiteNavigationElement und der Rest des langen Schwanzes, den keine Oberfläche konsumiert.

Prüfen

Zwei Werkzeuge, die verschiedene Fragen beantworten. Der Rich Results Test sagt Ihnen, ob eine Seite für einen bestimmten Ergebnistyp infrage kommt. Der Schema Markup Validator sagt Ihnen, ob Ihr Graph wohlgeformt ist - er ist der, der eine ins Leere zeigende @id-Referenz findet, die das erste Werkzeug fröhlich ignoriert.

Führen Sie beide aus, auf dem gerenderten HTML der ausgelieferten Seite und nicht auf einem eingefügten Schnipsel. Der Schnipsel ist, was Sie geschrieben haben; das gerenderte HTML ist, was Sie ausgeliefert haben - und auf Seiten mit einer Caching-Schicht davor sind das nicht zuverlässig dieselben Dinge.

Zurück zu allen Artikeln