cms.c2sgmbh/src/lib/structuredData.ts
Martin Porwoll 51c340e9e7 feat: add i18n, SEO, and frontend infrastructure
Localization:
- Add middleware for locale detection/routing
- Add [locale] dynamic route structure
- Add i18n utility library (DE/EN support)

SEO & Discovery:
- Add robots.ts for search engine directives
- Add sitemap.ts for XML sitemap generation
- Add structuredData.ts for JSON-LD schemas

Utilities:
- Add search.ts for full-text search functionality
- Add tenantAccess.ts for multi-tenant access control
- Add envValidation.ts for environment validation

Frontend:
- Update layout.tsx with locale support
- Update page.tsx for localized content
- Add API routes for frontend functionality
- Add instrumentation.ts for monitoring

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 08:19:35 +00:00

480 lines
9.8 KiB
TypeScript

/**
* Structured Data (JSON-LD) Helpers
*
* Diese Funktionen generieren Schema.org-konforme JSON-LD Daten
* für verschiedene Content-Typen.
*
* Verwendung in Next.js:
* ```tsx
* import { generateArticleSchema } from '@/lib/structuredData'
*
* export default function BlogPost({ post }) {
* return (
* <>
* <script
* type="application/ld+json"
* dangerouslySetInnerHTML={{
* __html: JSON.stringify(generateArticleSchema(post))
* }}
* />
* <article>...</article>
* </>
* )
* }
* ```
*/
export interface OrganizationData {
name: string
url: string
logo?: string
description?: string
email?: string
phone?: string
address?: {
street?: string
city?: string
postalCode?: string
country?: string
}
socialProfiles?: string[]
}
export interface ArticleData {
title: string
description?: string
slug: string
publishedAt?: string
updatedAt?: string
author?: string
featuredImage?: {
url?: string
alt?: string
width?: number
height?: number
}
categories?: string[]
}
export interface WebPageData {
title: string
description?: string
url: string
updatedAt?: string
image?: {
url?: string
alt?: string
}
}
export interface BreadcrumbItem {
name: string
url: string
}
export interface FAQItem {
question: string
answer: string
}
export interface TestimonialData {
author: string
quote: string
rating?: number
company?: string
date?: string
}
/**
* Generiert Organization Schema
*/
export function generateOrganizationSchema(data: OrganizationData) {
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'Organization',
name: data.name,
url: data.url,
}
if (data.logo) {
schema.logo = {
'@type': 'ImageObject',
url: data.logo,
}
}
if (data.description) {
schema.description = data.description
}
if (data.email) {
schema.email = data.email
}
if (data.phone) {
schema.telephone = data.phone
}
if (data.address) {
schema.address = {
'@type': 'PostalAddress',
streetAddress: data.address.street,
addressLocality: data.address.city,
postalCode: data.address.postalCode,
addressCountry: data.address.country || 'DE',
}
}
if (data.socialProfiles && data.socialProfiles.length > 0) {
schema.sameAs = data.socialProfiles
}
return schema
}
/**
* Generiert Article Schema für Blog-Posts und News
*/
export function generateArticleSchema(
data: ArticleData,
baseUrl: string,
organization?: OrganizationData
) {
const articleUrl = `${baseUrl}/blog/${data.slug}`
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: data.title,
url: articleUrl,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': articleUrl,
},
}
if (data.description) {
schema.description = data.description
}
if (data.publishedAt) {
schema.datePublished = data.publishedAt
}
if (data.updatedAt) {
schema.dateModified = data.updatedAt
}
if (data.author) {
schema.author = {
'@type': 'Person',
name: data.author,
}
} else if (organization) {
schema.author = {
'@type': 'Organization',
name: organization.name,
}
}
if (organization) {
schema.publisher = {
'@type': 'Organization',
name: organization.name,
url: organization.url,
...(organization.logo && {
logo: {
'@type': 'ImageObject',
url: organization.logo,
},
}),
}
}
if (data.featuredImage?.url) {
schema.image = {
'@type': 'ImageObject',
url: data.featuredImage.url,
...(data.featuredImage.alt && { caption: data.featuredImage.alt }),
...(data.featuredImage.width && { width: data.featuredImage.width }),
...(data.featuredImage.height && { height: data.featuredImage.height }),
}
}
if (data.categories && data.categories.length > 0) {
schema.keywords = data.categories.join(', ')
}
return schema
}
/**
* Generiert NewsArticle Schema für News und Pressemitteilungen
*/
export function generateNewsArticleSchema(
data: ArticleData,
baseUrl: string,
organization?: OrganizationData
) {
const baseSchema = generateArticleSchema(data, baseUrl, organization)
return {
...baseSchema,
'@type': 'NewsArticle',
}
}
/**
* Generiert WebPage Schema
*/
export function generateWebPageSchema(data: WebPageData, organization?: OrganizationData) {
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'WebPage',
name: data.title,
url: data.url,
}
if (data.description) {
schema.description = data.description
}
if (data.updatedAt) {
schema.dateModified = data.updatedAt
}
if (data.image?.url) {
schema.image = {
'@type': 'ImageObject',
url: data.image.url,
...(data.image.alt && { caption: data.image.alt }),
}
}
if (organization) {
schema.publisher = {
'@type': 'Organization',
name: organization.name,
}
}
return schema
}
/**
* Generiert BreadcrumbList Schema
*/
export function generateBreadcrumbSchema(items: BreadcrumbItem[]) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((item, index) => ({
'@type': 'ListItem',
position: index + 1,
name: item.name,
item: item.url,
})),
}
}
/**
* Generiert FAQPage Schema
*/
export function generateFAQSchema(items: FAQItem[]) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: items.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
}
}
/**
* Generiert Review/Testimonial Schema
*/
export function generateReviewSchema(
testimonial: TestimonialData,
organization: OrganizationData
) {
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'Review',
reviewBody: testimonial.quote,
author: {
'@type': 'Person',
name: testimonial.author,
},
itemReviewed: {
'@type': 'Organization',
name: organization.name,
},
}
if (testimonial.rating) {
schema.reviewRating = {
'@type': 'Rating',
ratingValue: testimonial.rating,
bestRating: 5,
worstRating: 1,
}
}
if (testimonial.date) {
schema.datePublished = testimonial.date
}
return schema
}
/**
* Generiert AggregateRating Schema für Testimonials
*/
export function generateAggregateRatingSchema(
testimonials: TestimonialData[],
organization: OrganizationData
) {
const ratingsWithScore = testimonials.filter((t) => t.rating)
if (ratingsWithScore.length === 0) return null
const totalRating = ratingsWithScore.reduce((sum, t) => sum + (t.rating || 0), 0)
const averageRating = totalRating / ratingsWithScore.length
return {
'@context': 'https://schema.org',
'@type': 'Organization',
name: organization.name,
url: organization.url,
aggregateRating: {
'@type': 'AggregateRating',
ratingValue: averageRating.toFixed(1),
bestRating: 5,
worstRating: 1,
ratingCount: ratingsWithScore.length,
},
}
}
/**
* Generiert LocalBusiness Schema
*/
export function generateLocalBusinessSchema(
data: OrganizationData & {
type?: string
openingHours?: string[]
priceRange?: string
geo?: {
latitude: number
longitude: number
}
}
) {
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': data.type || 'LocalBusiness',
name: data.name,
url: data.url,
}
if (data.logo) {
schema.image = data.logo
}
if (data.description) {
schema.description = data.description
}
if (data.email) {
schema.email = data.email
}
if (data.phone) {
schema.telephone = data.phone
}
if (data.address) {
schema.address = {
'@type': 'PostalAddress',
streetAddress: data.address.street,
addressLocality: data.address.city,
postalCode: data.address.postalCode,
addressCountry: data.address.country || 'DE',
}
}
if (data.openingHours && data.openingHours.length > 0) {
schema.openingHours = data.openingHours
}
if (data.priceRange) {
schema.priceRange = data.priceRange
}
if (data.geo) {
schema.geo = {
'@type': 'GeoCoordinates',
latitude: data.geo.latitude,
longitude: data.geo.longitude,
}
}
if (data.socialProfiles && data.socialProfiles.length > 0) {
schema.sameAs = data.socialProfiles
}
return schema
}
/**
* Generiert WebSite Schema mit SearchAction
*/
export function generateWebSiteSchema(
name: string,
url: string,
searchUrl?: string
) {
const schema: Record<string, unknown> = {
'@context': 'https://schema.org',
'@type': 'WebSite',
name,
url,
}
if (searchUrl) {
schema.potentialAction = {
'@type': 'SearchAction',
target: {
'@type': 'EntryPoint',
urlTemplate: `${searchUrl}?q={search_term_string}`,
},
'query-input': 'required name=search_term_string',
}
}
return schema
}
/**
* Kombiniert mehrere Schemas in ein Array
*/
export function combineSchemas(...schemas: (Record<string, unknown> | null)[]) {
const validSchemas = schemas.filter((s) => s !== null)
if (validSchemas.length === 0) return null
if (validSchemas.length === 1) return validSchemas[0]
return validSchemas
}
/**
* Hilfsfunktion zum sicheren Rendern von JSON-LD
*/
export function renderJsonLd(schema: Record<string, unknown> | Record<string, unknown>[] | null) {
if (!schema) return null
return JSON.stringify(schema, null, process.env.NODE_ENV === 'development' ? 2 : 0)
}