Skip to content

Structured Data

JSON-LD ist die technische Ausgabeform, schema.org das Vokabular. Beides steht in src/lib/seo/jsonld.ts: typisierte Builder, die je einen Knoten erzeugen.

Ein Graph je Seite

Alle Knoten einer Seite landen in einem gemeinsamen @graph und verweisen über @id aufeinander:

json
{
  "@context": "https://schema.org",
  "@graph": [
    {
      "@type": "Organization",
      "@id": "https://www.muster.de/#organization",
      "name": "Muster GmbH"
    },
    {
      "@type": "WebSite",
      "@id": "https://www.muster.de/#website",
      "publisher": { "@id": "https://www.muster.de/#organization" }
    },
    {
      "@type": "WebPage",
      "@id": "https://www.muster.de/leistungen/#webpage",
      "isPartOf": { "@id": "https://www.muster.de/#website" }
    }
  ]
}

Getrennte Skripte wären drei unverbundene Angaben. Im Graphen erkennt eine Suchmaschine, dass Organisation, Website und Seite dieselbe Sache beschreiben.

Was wann entsteht

features.structuredData ist der Hauptschalter — steht er auf false, entsteht kein einziges Skript. Darüber hinaus entscheiden die Flags unter seo.schemas:

KnotenBedingung
Organizationseo.schemas.organization
LocalBusinessseo.schemas.localBusiness — ersetzt den Typ, kein zweiter Knoten
WebSiteseo.schemas.website
WebPageimmer, sobald strukturierte Daten aktiv sind
BreadcrumbListwenn die Seite breadcrumbs übergibt
FAQPagewenn die Seite <Faq /> einsetzt

Organization und LocalBusiness sind ein Knoten

LocalBusiness ist eine Unterart von Organization. Zwei Knoten für dieselbe Firma würden sie zu zwei Firmen machen. Ist localBusiness aktiv, wechselt der Typ — und company.address wird zur Pflicht, sonst entstünde unbrauchbares JSON-LD.

Leere Felder entstehen nicht: Was nicht konfiguriert ist — sameAs, Telefon, Anschrift — fehlt im Knoten, statt als leere Angabe dazustehen.

Brotkrumen

Der sichtbare Pfad und die BreadcrumbList entstehen aus einer Liste:

astro
<BaseLayout
  title="Beratung"
  breadcrumbs={[
    { label: 'Startseite', href: '/' },
    { label: 'Leistungen', href: '/leistungen' },
    { label: 'Beratung' },
  ]}
/>

Der letzte Eintrag ist die aktuelle Seite und trägt kein href: Ein Verweis auf die Seite, auf der man schon ist, führt nirgendwohin und wird von Screenreadern trotzdem als Ziel angesagt. Er bekommt stattdessen aria-current="page".

Häufige Fragen

astro
---
import Faq from '../components/seo/Faq.astro';
---

<Faq
  titel="Häufige Fragen"
  eintraege={[{ frage: 'Wie lange dauert es?', antwort: 'Etwa zwei Wochen.' }]}
/>

Die Komponente rendert die Fragen als Klappabschnitte und zeichnet dieselbe Liste als FAQPage aus. Antworten sind reiner Text — wer Auszeichnung in der Antwort braucht, hat keine FAQ, sondern einen Artikel.

Auszeichnung ohne sichtbaren Inhalt ist ein Verstoß

Google ahndet FAQ-Auszeichnung ohne sichtbare Fragen mit einer manuellen Maßnahme, und die trifft die ganze Domain. Deshalb gibt es keinen Weg, die Auszeichnung ohne die Anzeige zu bekommen.

Seitenbezogene Knoten

Für Inhalte, die es im Starter noch nicht gibt, stehen typisierte Builder bereit. Sie werden über jsonLd übergeben:

astro
---
import { articleNode } from '../lib/seo/jsonld';
import project from 'virtual:project-config';
import { canonicalUrl, organizationId } from '../lib/seo';

const canonical = canonicalUrl(project.site.url, Astro.url.pathname);
---

<BaseLayout
  title="Ein Beitrag"
  ogType="article"
  jsonLd={[
    articleNode({
      headline: 'Ein Beitrag',
      description: 'Worum es geht.',
      canonical,
      datePublished: '2026-07-29',
      authorName: 'Redaktion',
      publisherId: organizationId(project.site.url),
    }),
  ]}
/>

Verfügbar sind articleNode, serviceNode und faqPageNode. Sie sind einzeln getestet, aber im Starter noch an keiner Seite im Einsatz — es gibt dort keinen passenden sichtbaren Inhalt.

Prüfen

Der Post-Build-Test stellt sicher, dass jeder Block gültiges JSON ist, den richtigen Kontext trägt und nur die konfigurierten Schemata enthält. Für die inhaltliche Prüfung vor dem Livegang: