Initial commit: Portfolio Website
Vollständige Next.js 15 Portfolio-Website mit: - Blog-System mit 100+ Artikeln - Supabase-Integration - Responsive Design mit Tailwind CSS - TypeScript-Konfiguration - Testing-Setup mit Vitest und Playwright Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,978 @@
|
||||
# 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/)
|
||||
Reference in New Issue
Block a user