- Add 100 blog posts covering AI, development, and tech topics - Add .env.example for environment configuration - Add accessibility and lighthouse audit scripts - Remove obsolete SEO reports and temporary files - Remove dev-dist build artifacts and backup files - Remove unused portrait images (moved/consolidated elsewhere) - Update contact form and component improvements Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
979 lines
25 KiB
Markdown
979 lines
25 KiB
Markdown
# 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/)
|