# 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

```typescript
// 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

```tsx
// 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" />;
}
```

```tsx
// 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

```tsx
// 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

```tsx
// 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

```typescript
// 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 Type | Rich Result | Verwendung |
|-------------|-------------|------------|
| **Article** | Article snippet | Blog, News |
| **Product** | Product snippet | E-Commerce |
| **FAQPage** | FAQ accordion | Support, Info |
| **HowTo** | Step-by-step | Tutorials |
| **Event** | Event listing | Veranstaltungen |
| **LocalBusiness** | Knowledge panel | Lokale Firmen |
| **BreadcrumbList** | Breadcrumb trail | Navigation |
| **Organization** | Knowledge panel | Unternehmen |
| **Person** | Knowledge panel | Personen |
| **Recipe** | Recipe card | Kochrezepte |
| **JobPosting** | Job listing | Stellenanzeigen |
| **Course** | Course info | Online-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

- [Schema.org Documentation](https://schema.org/)
- [Google Structured Data Guidelines](https://developers.google.com/search/docs/appearance/structured-data)
- [Google Rich Results Test](https://search.google.com/test/rich-results)
- [JSON-LD Specification](https://json-ld.org/)
