Menu
Nazad na Blog
2 min read
SEO

Structured Data & JSON-LD

Strukturierte Daten mit JSON-LD für SEO. Schema.org Typen, Rich Snippets und Google Rich Results für bessere Sichtbarkeit.

JSON-LDStructured DataSchema.orgRich SnippetsSEOGoogle Rich Results
Structured Data & JSON-LD

Structured Data & JSON-LD

Meta-Description: Strukturierte Daten mit JSON-LD für SEO. Schema.org Typen, Rich Snippets und Google Rich Results für bessere Sichtbarkeit.

Keywords: JSON-LD, Structured Data, Schema.org, Rich Snippets, SEO, Google Rich Results, Semantic Web


Einführung

JSON-LD (JavaScript Object Notation for Linked Data) ist Googles bevorzugtes Format für strukturierte Daten. Es ermöglicht Rich Snippets in Suchergebnissen – von Sternebewertungen bis FAQs. Dieser Guide zeigt die wichtigsten Schema-Typen für moderne Websites.


JSON-LD Overview

┌─────────────────────────────────────────────────────────────┐
│              STRUCTURED DATA ECOSYSTEM                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  Schema.org Hierarchy:                                      │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  Thing (Base Type)                                  │   │
│  │  ├── CreativeWork                                   │   │
│  │  │   ├── Article                                    │   │
│  │  │   ├── BlogPosting                                │   │
│  │  │   ├── WebPage                                    │   │
│  │  │   └── SoftwareApplication                        │   │
│  │  ├── Organization                                   │   │
│  │  │   ├── LocalBusiness                              │   │
│  │  │   └── Corporation                                │   │
│  │  ├── Person                                         │   │
│  │  ├── Product                                        │   │
│  │  ├── Event                                          │   │
│  │  └── Place                                          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Rich Results Types:                                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  ⭐ Review Snippets (Sternebewertungen)             │   │
│  │  ❓ FAQ Snippets (Accordion Q&A)                    │   │
│  │  📝 How-To Snippets (Schritt-für-Schritt)          │   │
│  │  🛒 Product Snippets (Preis, Verfügbarkeit)        │   │
│  │  📅 Event Snippets (Datum, Ort)                    │   │
│  │  👤 Person Knowledge Panel                          │   │
│  │  🏢 Organization Knowledge Panel                    │   │
│  │  🍳 Recipe Cards (Kochzeit, Kalorien)              │   │
│  │  💼 Job Postings (Gehalt, Standort)                │   │
│  │  🔍 Sitelinks Searchbox                             │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  Implementation:                                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  <script type="application/ld+json">                │   │
│  │    {                                                │   │
│  │      "@context": "https://schema.org",              │   │
│  │      "@type": "Article",                            │   │
│  │      "headline": "...",                             │   │
│  │      "author": { "@type": "Person", ... }           │   │
│  │    }                                                │   │
│  │  </script>                                          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘

TypeScript JSON-LD Library

// lib/jsonld/types.ts - Type Definitions
export interface Thing {
  '@context': 'https://schema.org';
  '@type': string;
  name?: string;
  description?: string;
  url?: string;
  image?: string | ImageObject;
}

export interface ImageObject {
  '@type': 'ImageObject';
  url: string;
  width?: number;
  height?: number;
  caption?: string;
}

export interface Person {
  '@type': 'Person';
  name: string;
  url?: string;
  image?: string;
  jobTitle?: string;
  worksFor?: Organization;
  sameAs?: string[];
}

export interface Organization {
  '@type': 'Organization' | 'LocalBusiness' | 'Corporation';
  name: string;
  url?: string;
  logo?: string | ImageObject;
  sameAs?: string[];
  contactPoint?: ContactPoint[];
  address?: PostalAddress;
}

export interface ContactPoint {
  '@type': 'ContactPoint';
  telephone: string;
  contactType: string;
  availableLanguage?: string[];
}

export interface PostalAddress {
  '@type': 'PostalAddress';
  streetAddress?: string;
  addressLocality?: string;
  addressRegion?: string;
  postalCode?: string;
  addressCountry?: string;
}

export interface Article {
  '@context': 'https://schema.org';
  '@type': 'Article' | 'BlogPosting' | 'NewsArticle' | 'TechArticle';
  headline: string;
  description?: string;
  image?: string | string[];
  datePublished: string;
  dateModified?: string;
  author: Person | Person[];
  publisher: Organization;
  mainEntityOfPage?: WebPage;
  articleBody?: string;
  wordCount?: number;
  keywords?: string[];
}

export interface WebPage {
  '@type': 'WebPage';
  '@id': string;
}

export interface Product {
  '@context': 'https://schema.org';
  '@type': 'Product';
  name: string;
  description?: string;
  image?: string | string[];
  brand?: Brand;
  offers?: Offer | Offer[];
  aggregateRating?: AggregateRating;
  review?: Review[];
  sku?: string;
  gtin13?: string;
}

export interface Brand {
  '@type': 'Brand';
  name: string;
}

export interface Offer {
  '@type': 'Offer';
  price: number;
  priceCurrency: string;
  availability: 'https://schema.org/InStock' | 'https://schema.org/OutOfStock' | 'https://schema.org/PreOrder';
  url?: string;
  priceValidUntil?: string;
  seller?: Organization;
}

export interface AggregateRating {
  '@type': 'AggregateRating';
  ratingValue: number;
  reviewCount: number;
  bestRating?: number;
  worstRating?: number;
}

export interface Review {
  '@type': 'Review';
  author: Person;
  datePublished: string;
  reviewBody?: string;
  reviewRating: Rating;
}

export interface Rating {
  '@type': 'Rating';
  ratingValue: number;
  bestRating?: number;
  worstRating?: number;
}

export interface FAQPage {
  '@context': 'https://schema.org';
  '@type': 'FAQPage';
  mainEntity: Question[];
}

export interface Question {
  '@type': 'Question';
  name: string;
  acceptedAnswer: Answer;
}

export interface Answer {
  '@type': 'Answer';
  text: string;
}

export interface BreadcrumbList {
  '@context': 'https://schema.org';
  '@type': 'BreadcrumbList';
  itemListElement: ListItem[];
}

export interface ListItem {
  '@type': 'ListItem';
  position: number;
  name: string;
  item?: string;
}

export interface HowTo {
  '@context': 'https://schema.org';
  '@type': 'HowTo';
  name: string;
  description?: string;
  image?: string;
  totalTime?: string; // ISO 8601 duration
  estimatedCost?: MonetaryAmount;
  supply?: HowToSupply[];
  tool?: HowToTool[];
  step: HowToStep[];
}

export interface HowToStep {
  '@type': 'HowToStep';
  name: string;
  text: string;
  image?: string;
  url?: string;
}

export interface HowToSupply {
  '@type': 'HowToSupply';
  name: string;
}

export interface HowToTool {
  '@type': 'HowToTool';
  name: string;
}

export interface MonetaryAmount {
  '@type': 'MonetaryAmount';
  currency: string;
  value: number;
}

export interface Event {
  '@context': 'https://schema.org';
  '@type': 'Event' | 'BusinessEvent' | 'MusicEvent' | 'SportsEvent';
  name: string;
  description?: string;
  startDate: string;
  endDate?: string;
  location: Place | VirtualLocation;
  organizer?: Organization | Person;
  performer?: Person | Organization;
  offers?: Offer[];
  eventStatus?: 'https://schema.org/EventScheduled' | 'https://schema.org/EventCancelled' | 'https://schema.org/EventPostponed';
  eventAttendanceMode?: 'https://schema.org/OnlineEventAttendanceMode' | 'https://schema.org/OfflineEventAttendanceMode' | 'https://schema.org/MixedEventAttendanceMode';
}

export interface Place {
  '@type': 'Place';
  name: string;
  address: PostalAddress;
}

export interface VirtualLocation {
  '@type': 'VirtualLocation';
  url: string;
}

JSON-LD Components

// components/jsonld/JsonLd.tsx - Base Component
import Script from 'next/script';

interface JsonLdProps<T> {
  data: T;
  id?: string;
}

export function JsonLd<T extends object>({ data, id }: JsonLdProps<T>) {
  return (
    <Script
      id={id || 'json-ld'}
      type="application/ld+json"
      dangerouslySetInnerHTML={{
        __html: JSON.stringify(data, null, 0)
      }}
      strategy="beforeInteractive"
    />
  );
}

// components/jsonld/ArticleJsonLd.tsx
import { JsonLd } from './JsonLd';
import type { Article, Person, Organization } from '@/lib/jsonld/types';

interface ArticleJsonLdProps {
  title: string;
  description: string;
  publishedTime: string;
  modifiedTime?: string;
  authorName: string;
  authorUrl?: string;
  images: string[];
  url: string;
  siteName: string;
  logoUrl: string;
}

export function ArticleJsonLd({
  title,
  description,
  publishedTime,
  modifiedTime,
  authorName,
  authorUrl,
  images,
  url,
  siteName,
  logoUrl
}: ArticleJsonLdProps) {
  const author: Person = {
    '@type': 'Person',
    name: authorName,
    ...(authorUrl && { url: authorUrl })
  };

  const publisher: Organization = {
    '@type': 'Organization',
    name: siteName,
    logo: {
      '@type': 'ImageObject',
      url: logoUrl,
      width: 600,
      height: 60
    }
  };

  const data: Article = {
    '@context': 'https://schema.org',
    '@type': 'Article',
    headline: title,
    description,
    image: images,
    datePublished: publishedTime,
    ...(modifiedTime && { dateModified: modifiedTime }),
    author,
    publisher,
    mainEntityOfPage: {
      '@type': 'WebPage',
      '@id': url
    }
  };

  return <JsonLd data={data} id="article-jsonld" />;
}

// components/jsonld/ProductJsonLd.tsx
import { JsonLd } from './JsonLd';
import type { Product, Offer, AggregateRating } from '@/lib/jsonld/types';

interface ProductJsonLdProps {
  name: string;
  description: string;
  images: string[];
  brand: string;
  sku: string;
  price: number;
  currency: string;
  availability: 'InStock' | 'OutOfStock' | 'PreOrder';
  ratingValue?: number;
  reviewCount?: number;
  url: string;
}

export function ProductJsonLd({
  name,
  description,
  images,
  brand,
  sku,
  price,
  currency,
  availability,
  ratingValue,
  reviewCount,
  url
}: ProductJsonLdProps) {
  const offer: Offer = {
    '@type': 'Offer',
    price,
    priceCurrency: currency,
    availability: `https://schema.org/${availability}`,
    url
  };

  const data: Product = {
    '@context': 'https://schema.org',
    '@type': 'Product',
    name,
    description,
    image: images,
    brand: {
      '@type': 'Brand',
      name: brand
    },
    sku,
    offers: offer,
    ...(ratingValue && reviewCount && {
      aggregateRating: {
        '@type': 'AggregateRating',
        ratingValue,
        reviewCount,
        bestRating: 5,
        worstRating: 1
      } as AggregateRating
    })
  };

  return <JsonLd data={data} id="product-jsonld" />;
}
// components/jsonld/FAQJsonLd.tsx
import { JsonLd } from './JsonLd';
import type { FAQPage, Question } from '@/lib/jsonld/types';

interface FAQItem {
  question: string;
  answer: string;
}

interface FAQJsonLdProps {
  items: FAQItem[];
}

export function FAQJsonLd({ items }: FAQJsonLdProps) {
  const questions: Question[] = items.map(item => ({
    '@type': 'Question',
    name: item.question,
    acceptedAnswer: {
      '@type': 'Answer',
      text: item.answer
    }
  }));

  const data: FAQPage = {
    '@context': 'https://schema.org',
    '@type': 'FAQPage',
    mainEntity: questions
  };

  return <JsonLd data={data} id="faq-jsonld" />;
}

// components/jsonld/BreadcrumbJsonLd.tsx
import { JsonLd } from './JsonLd';
import type { BreadcrumbList, ListItem } from '@/lib/jsonld/types';

interface BreadcrumbItem {
  name: string;
  url?: string;
}

interface BreadcrumbJsonLdProps {
  items: BreadcrumbItem[];
}

export function BreadcrumbJsonLd({ items }: BreadcrumbJsonLdProps) {
  const listItems: ListItem[] = items.map((item, index) => ({
    '@type': 'ListItem',
    position: index + 1,
    name: item.name,
    ...(item.url && { item: item.url })
  }));

  const data: BreadcrumbList = {
    '@context': 'https://schema.org',
    '@type': 'BreadcrumbList',
    itemListElement: listItems
  };

  return <JsonLd data={data} id="breadcrumb-jsonld" />;
}

// components/jsonld/OrganizationJsonLd.tsx
import { JsonLd } from './JsonLd';
import type { Organization, ContactPoint, PostalAddress } from '@/lib/jsonld/types';

interface OrganizationJsonLdProps {
  name: string;
  url: string;
  logo: string;
  description?: string;
  email?: string;
  telephone?: string;
  address?: {
    street: string;
    city: string;
    region: string;
    postalCode: string;
    country: string;
  };
  socialProfiles?: string[];
}

export function OrganizationJsonLd({
  name,
  url,
  logo,
  description,
  email,
  telephone,
  address,
  socialProfiles
}: OrganizationJsonLdProps) {
  const contactPoints: ContactPoint[] = [];

  if (telephone) {
    contactPoints.push({
      '@type': 'ContactPoint',
      telephone,
      contactType: 'customer service',
      availableLanguage: ['German', 'English']
    });
  }

  const postalAddress: PostalAddress | undefined = address ? {
    '@type': 'PostalAddress',
    streetAddress: address.street,
    addressLocality: address.city,
    addressRegion: address.region,
    postalCode: address.postalCode,
    addressCountry: address.country
  } : undefined;

  const data: Organization & { '@context': string; description?: string } = {
    '@context': 'https://schema.org',
    '@type': 'Organization',
    name,
    url,
    logo: {
      '@type': 'ImageObject',
      url: logo,
      width: 600,
      height: 60
    },
    ...(description && { description }),
    ...(contactPoints.length > 0 && { contactPoint: contactPoints }),
    ...(postalAddress && { address: postalAddress }),
    ...(socialProfiles && { sameAs: socialProfiles })
  };

  return <JsonLd data={data} id="organization-jsonld" />;
}

Practical Examples

// app/blog/[slug]/page.tsx - Blog Article with JSON-LD
import { ArticleJsonLd } from '@/components/jsonld/ArticleJsonLd';
import { BreadcrumbJsonLd } from '@/components/jsonld/BreadcrumbJsonLd';
import { getBlogPost } from '@/lib/blog';

export default async function BlogPost({
  params
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params;
  const post = await getBlogPost(slug);

  const baseUrl = 'https://example.com';

  return (
    <>
      <ArticleJsonLd
        title={post.title}
        description={post.excerpt}
        publishedTime={post.publishedAt}
        modifiedTime={post.updatedAt}
        authorName={post.author.name}
        authorUrl={`${baseUrl}/author/${post.author.slug}`}
        images={[post.coverImage]}
        url={`${baseUrl}/blog/${slug}`}
        siteName="Meine Website"
        logoUrl={`${baseUrl}/logo.png`}
      />

      <BreadcrumbJsonLd
        items={[
          { name: 'Home', url: baseUrl },
          { name: 'Blog', url: `${baseUrl}/blog` },
          { name: post.title }
        ]}
      />

      <article>
        <h1>{post.title}</h1>
        <div dangerouslySetInnerHTML={{ __html: post.content }} />
      </article>
    </>
  );
}

// app/products/[slug]/page.tsx - Product with JSON-LD
import { ProductJsonLd } from '@/components/jsonld/ProductJsonLd';
import { BreadcrumbJsonLd } from '@/components/jsonld/BreadcrumbJsonLd';
import { FAQJsonLd } from '@/components/jsonld/FAQJsonLd';
import { getProduct } from '@/lib/products';

export default async function ProductPage({
  params
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params;
  const product = await getProduct(slug);

  const baseUrl = 'https://example.com';

  // Produkt-FAQs
  const faqs = [
    {
      question: 'Wie lange ist die Lieferzeit?',
      answer: 'Die Lieferzeit beträgt 2-3 Werktage innerhalb Deutschlands.'
    },
    {
      question: 'Gibt es eine Garantie?',
      answer: 'Ja, alle Produkte haben 2 Jahre Herstellergarantie.'
    }
  ];

  return (
    <>
      <ProductJsonLd
        name={product.name}
        description={product.description}
        images={product.images}
        brand={product.brand}
        sku={product.sku}
        price={product.price}
        currency="EUR"
        availability={product.inStock ? 'InStock' : 'OutOfStock'}
        ratingValue={product.rating}
        reviewCount={product.reviewCount}
        url={`${baseUrl}/products/${slug}`}
      />

      <BreadcrumbJsonLd
        items={[
          { name: 'Home', url: baseUrl },
          { name: 'Produkte', url: `${baseUrl}/products` },
          { name: product.category, url: `${baseUrl}/products?category=${product.categorySlug}` },
          { name: product.name }
        ]}
      />

      <FAQJsonLd items={faqs} />

      <main>
        <h1>{product.name}</h1>
        {/* Product content */}
      </main>
    </>
  );
}

Advanced Schemas

// components/jsonld/HowToJsonLd.tsx
import { JsonLd } from './JsonLd';
import type { HowTo, HowToStep } from '@/lib/jsonld/types';

interface Step {
  name: string;
  text: string;
  image?: string;
}

interface HowToJsonLdProps {
  name: string;
  description: string;
  image?: string;
  totalTime?: string; // e.g., "PT30M" for 30 minutes
  steps: Step[];
}

export function HowToJsonLd({
  name,
  description,
  image,
  totalTime,
  steps
}: HowToJsonLdProps) {
  const howToSteps: HowToStep[] = steps.map((step, index) => ({
    '@type': 'HowToStep',
    name: step.name,
    text: step.text,
    ...(step.image && { image: step.image }),
    url: `#step-${index + 1}`
  }));

  const data: HowTo = {
    '@context': 'https://schema.org',
    '@type': 'HowTo',
    name,
    description,
    ...(image && { image }),
    ...(totalTime && { totalTime }),
    step: howToSteps
  };

  return <JsonLd data={data} id="howto-jsonld" />;
}

// components/jsonld/EventJsonLd.tsx
import { JsonLd } from './JsonLd';
import type { Event, Place, VirtualLocation, Offer } from '@/lib/jsonld/types';

interface EventJsonLdProps {
  name: string;
  description: string;
  startDate: string;
  endDate?: string;
  location: {
    type: 'physical' | 'virtual' | 'hybrid';
    name?: string;
    address?: {
      street: string;
      city: string;
      region: string;
      postalCode: string;
      country: string;
    };
    url?: string;
  };
  organizer: {
    name: string;
    url: string;
  };
  image?: string;
  offers?: {
    price: number;
    currency: string;
    url: string;
    validFrom?: string;
  };
  status?: 'scheduled' | 'cancelled' | 'postponed';
}

export function EventJsonLd({
  name,
  description,
  startDate,
  endDate,
  location,
  organizer,
  image,
  offers,
  status = 'scheduled'
}: EventJsonLdProps) {
  const eventLocation = location.type === 'virtual' ? {
    '@type': 'VirtualLocation' as const,
    url: location.url!
  } : {
    '@type': 'Place' as const,
    name: location.name!,
    address: {
      '@type': 'PostalAddress' as const,
      streetAddress: location.address!.street,
      addressLocality: location.address!.city,
      addressRegion: location.address!.region,
      postalCode: location.address!.postalCode,
      addressCountry: location.address!.country
    }
  };

  const attendanceMode = {
    physical: 'https://schema.org/OfflineEventAttendanceMode',
    virtual: 'https://schema.org/OnlineEventAttendanceMode',
    hybrid: 'https://schema.org/MixedEventAttendanceMode'
  }[location.type] as Event['eventAttendanceMode'];

  const eventStatus = {
    scheduled: 'https://schema.org/EventScheduled',
    cancelled: 'https://schema.org/EventCancelled',
    postponed: 'https://schema.org/EventPostponed'
  }[status] as Event['eventStatus'];

  const data: Event & { image?: string } = {
    '@context': 'https://schema.org',
    '@type': 'Event',
    name,
    description,
    startDate,
    ...(endDate && { endDate }),
    location: eventLocation,
    organizer: {
      '@type': 'Organization',
      name: organizer.name,
      url: organizer.url
    },
    eventAttendanceMode: attendanceMode,
    eventStatus,
    ...(image && { image }),
    ...(offers && {
      offers: {
        '@type': 'Offer',
        price: offers.price,
        priceCurrency: offers.currency,
        url: offers.url,
        availability: 'https://schema.org/InStock',
        ...(offers.validFrom && { validFrom: offers.validFrom })
      }
    })
  };

  return <JsonLd data={data} id="event-jsonld" />;
}

Validation & Testing

// lib/jsonld/validator.ts - Schema Validation
import Ajv from 'ajv';
import addFormats from 'ajv-formats';

const ajv = new Ajv({ allErrors: true });
addFormats(ajv);

// Article Schema
const articleSchema = {
  type: 'object',
  required: ['@context', '@type', 'headline', 'datePublished', 'author', 'publisher'],
  properties: {
    '@context': { const: 'https://schema.org' },
    '@type': { enum: ['Article', 'BlogPosting', 'NewsArticle', 'TechArticle'] },
    headline: { type: 'string', maxLength: 110 },
    description: { type: 'string', maxLength: 320 },
    image: {
      oneOf: [
        { type: 'string', format: 'uri' },
        { type: 'array', items: { type: 'string', format: 'uri' } }
      ]
    },
    datePublished: { type: 'string', format: 'date-time' },
    dateModified: { type: 'string', format: 'date-time' },
    author: {
      type: 'object',
      required: ['@type', 'name'],
      properties: {
        '@type': { const: 'Person' },
        name: { type: 'string' },
        url: { type: 'string', format: 'uri' }
      }
    },
    publisher: {
      type: 'object',
      required: ['@type', 'name', 'logo'],
      properties: {
        '@type': { const: 'Organization' },
        name: { type: 'string' },
        logo: { type: 'object' }
      }
    }
  }
};

export function validateArticleSchema(data: unknown): { valid: boolean; errors?: string[] } {
  const validate = ajv.compile(articleSchema);
  const valid = validate(data);

  return {
    valid,
    errors: validate.errors?.map(e => `${e.instancePath} ${e.message}`)
  };
}

// Test in Development
// app/api/validate-jsonld/route.ts
import { NextResponse } from 'next/server';
import { validateArticleSchema } from '@/lib/jsonld/validator';

export async function POST(request: Request) {
  const data = await request.json();
  const result = validateArticleSchema(data);

  return NextResponse.json(result);
}

Schema Type Reference

Schema TypeRich ResultVerwendung
**Article**Article snippetBlog, News
**Product**Product snippetE-Commerce
**FAQPage**FAQ accordionSupport, Info
**HowTo**Step-by-stepTutorials
**Event**Event listingVeranstaltungen
**LocalBusiness**Knowledge panelLokale Firmen
**BreadcrumbList**Breadcrumb trailNavigation
**Organization**Knowledge panelUnternehmen
**Person**Knowledge panelPersonen
**Recipe**Recipe cardKochrezepte
**JobPosting**Job listingStellenanzeigen
**Course**Course infoOnline-Kurse

Fazit

Strukturierte Daten mit JSON-LD bieten:

  1. Rich Snippets: Bessere Sichtbarkeit in Suchergebnissen
  2. Type Safety: TypeScript Definitionen für Schema.org
  3. Reusable Components: Wiederverwendbare React Komponenten
  4. Validation: Schema-Validierung für Qualitätssicherung

Strukturierte Daten sind essentiell für moderne SEO.


Bildprompts

  1. "Google search results showing rich snippets, FAQ accordion, product ratings"
  2. "Schema.org hierarchy diagram, connected nodes visualization"
  3. "JSON-LD code in browser developer tools, structured data testing"

Quellen