mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +00:00
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>
480 lines
9.8 KiB
TypeScript
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)
|
|
}
|