cms.c2sgmbh/docs/zweitmeinung/migration-strapi-to-payload.md
Martin Porwoll b62ca46133 chore: add zweitmeinung migration docs and GitHub protection script
- docs/zweitmeinung/: Migration guide (Strapi → Payload), content
  inventory, website guide, and reference screenshots
- scripts/setup-github-protection.sh: Branch protection + Dependabot
  auto-merge setup for cms.c2sgmbh repo

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-23 07:16:15 +00:00

1618 lines
45 KiB
Markdown

# Migration: Strapi zu Payload CMS
## Projekt: zweitmeinu.ng
**Erstellt:** 2026-02-20
**Strapi Version:** 5.x
**Ziel:** Payload CMS 3.x
---
## Inhaltsverzeichnis
1. [Executive Summary](#1-executive-summary)
2. [Content-Types / Collections](#2-content-types--collections)
3. [Components / Blocks](#3-components--blocks)
4. [API-Struktur](#4-api-struktur)
5. [Plugins & Extensions](#5-plugins--extensions)
6. [Datenbank & Relationen](#6-datenbank--relationen)
7. [Migrationsplan](#7-migrationsplan)
8. [Payload CMS Schema-Entwürfe](#8-payload-cms-schema-entwürfe)
---
## 1. Executive Summary
### Projektumfang
| Kategorie | Anzahl |
|-----------|--------|
| Content-Types | 16 |
| Components | 43 |
| API-Module | 18 |
| Plugins | 16 |
| Custom Controller Methods | 14 |
| Custom Services | 10 |
| Relationen | 25+ |
### Komplexitätsbewertung: **MITTEL-HOCH**
Hauptkomplexität liegt im:
- Termin-Buchungssystem (Appointment)
- Multi-Site-Konfiguration
- Dynamic Zone Page Builder
- Verschachtelte Component-Hierarchien
---
## 2. Content-Types / Collections
### 2.1 Übersicht aller Content-Types
| # | Content-Type | Draft/Publish | Felder | Relationen | Komplexität |
|---|--------------|---------------|--------|------------|-------------|
| 1 | appointment | Nein | 26 | 3 | Hoch |
| 2 | appointment-exception | Nein | 15 | 2 | Mittel |
| 3 | appointment-slot | Nein | 16 | 3 | Mittel |
| 4 | blog-category | Ja | 11 | 2 | Niedrig |
| 5 | blog-post | Ja | 15 | 2 | Mittel |
| 6 | contact-message | Nein | 13 | 1 | Niedrig |
| 7 | expert | Ja | 16 | 2 | Mittel |
| 8 | faq | Ja | 17 | 5 | Mittel |
| 9 | faq-category | Nein | 8 | 2 | Niedrig |
| 10 | faq-feedback | Nein | 15 | 3 | Niedrig |
| 11 | legal-page | Ja | 7 | 0 | Niedrig |
| 12 | news-item | Ja | 15 | 2 | Mittel |
| 13 | page | Ja | 5 | 1 | Hoch (Dynamic Zone) |
| 14 | service | Ja | 14 | 2 | Mittel |
| 15 | site-configuration | Ja | 25 | 5 | Hoch |
| 16 | waiting-list | Nein | 20 | 3 | Mittel |
---
### 2.2 Detaillierte Content-Type Definitionen
#### APPOINTMENT (Terminbuchungen)
```
Felder:
├── appointmentId (UUID, unique)
├── patientName (string, required, max 100)
├── patientEmail (email, required)
├── patientPhone (string, required, max 30)
├── patientDateOfBirth (date)
├── patientInsurance (string, max 100)
├── appointmentDate (date, required)
├── appointmentTime (time, required)
├── duration (integer, 15-240, default 30)
├── appointmentType (enum: consultation, examination, treatment, followup, vaccination, other)
├── status (enum: requested, confirmed, cancelled, completed, no-show)
├── confirmationToken (string, unique, private)
├── cancellationToken (string, unique, private)
├── cancellationReason (text, max 500)
├── notes (text, max 1000)
├── internalNotes (text, private)
├── reminderSent (boolean, private)
├── confirmationSent (boolean, private)
├── patientConsent (boolean, required)
├── requestedAt (datetime, private)
├── confirmedAt (datetime, private)
├── cancelledAt (datetime, private)
├── completedAt (datetime, private)
├── ipAddress (string, private)
└── userAgent (string, private)
Relationen:
├── expert → Expert (manyToOne)
├── service → Service (manyToOne)
└── site → Site-Configuration (manyToOne)
```
#### APPOINTMENT-EXCEPTION (Ausnahmen/Urlaub)
```
Felder:
├── type (enum: holiday, vacation, sick, conference, training, blocked, other)
├── startDate (datetime, required)
├── endDate (datetime, required)
├── allDay (boolean, default false)
├── reason (string, required, max 200)
├── description (text, max 500)
├── affectsAllSites (boolean, default false)
├── recurring (boolean, default false)
├── recurringPattern (enum: daily, weekly, monthly, yearly)
├── recurringEndDate (date)
├── isActive (boolean, default true)
├── notifyPatients (boolean, default true)
└── color (string, hex format)
Relationen:
├── expert → Expert (manyToOne, required)
└── site → Site-Configuration (manyToOne)
```
#### APPOINTMENT-SLOT (Verfügbare Zeitslots)
```
Felder:
├── dayOfWeek (enum: Mo-So, required)
├── startTime (time, required)
├── endTime (time, required)
├── slotDuration (integer, 15-240, default 30)
├── maxAppointments (integer, 1-10, default 1)
├── isActive (boolean, default true)
├── validFrom (date)
├── validUntil (date)
├── breakAfter (integer, 0-60, default 0)
├── bufferBefore (integer, 0-60, default 0)
├── bufferAfter (integer, 0-60, default 0)
├── color (string, hex)
└── notes (text, max 500)
Relationen:
├── expert → Expert (manyToOne, required)
├── site → Site-Configuration (manyToOne, required)
└── services ↔ Service (manyToMany)
```
#### BLOG-CATEGORY
```
Felder:
├── name (string, required, unique, max 100)
├── slug (uid, required, targetField: name)
├── description (text, max 500)
├── color (string, hex, default #0070f3)
├── icon (string, max 50)
├── order (integer, default 0)
├── isActive (boolean, default true)
└── seo (component: shared.seo)
Relationen:
├── blogPosts ← Blog-Post (oneToMany)
├── parent → Blog-Category (manyToOne, self-referential)
└── children ← Blog-Category (oneToMany, self-referential)
```
#### BLOG-POST
```
Felder:
├── title (string, required, max 200)
├── slug (uid, required, targetField: title)
├── excerpt (text, required, max 300)
├── content (richtext, required)
├── featuredImage (media, images only)
├── author (component: blog.author)
├── tags (json)
├── publishedAt (datetime)
├── readingTime (integer, min 1)
├── featured (boolean, default false)
├── relatedPosts (json)
└── seo (component: shared.seo)
Relationen:
├── category → Blog-Category (manyToOne)
└── sites ↔ Site-Configuration (manyToMany)
```
#### CONTACT-MESSAGE
```
Felder:
├── messageId (UUID, unique)
├── name (string, required, max 100)
├── email (email, required)
├── phone (string, max 30)
├── subject (string, required, max 200)
├── message (text, required, max 5000)
├── status (enum: new, read, replied, spam)
├── consentGiven (boolean, required)
├── submittedAt (datetime, private)
├── ipAddress (string, private)
└── userAgent (string, private)
Relationen:
└── site → Site-Configuration (manyToOne)
```
#### EXPERT (Ärzte/Spezialisten)
```
Felder:
├── expertId (UUID, unique)
├── firstName (string, required)
├── lastName (string, required)
├── slug (uid, required, targetField: lastName)
├── title (string)
├── specialties (json)
├── bio (richtext)
├── image (media, images only)
├── qualifications (component: expert.qualification, repeatable)
├── availability (component: expert.availability)
├── contactInfo (json)
├── isActive (boolean, default true)
├── order (integer, default 0)
└── seo (component: shared.seo)
Relationen:
├── services ↔ Service (manyToMany)
└── sites ↔ Site-Configuration (manyToMany)
```
#### FAQ
```
Felder:
├── question (string, required, max 300)
├── slug (uid, required, targetField: question)
├── answer (richtext, required)
├── shortAnswer (text, max 500)
├── tags (json)
├── priority (enum: low, medium, high, featured)
├── viewCount (integer, private)
├── helpfulCount (integer)
├── notHelpfulCount (integer)
├── attachments (media, multiple)
├── videoUrl (string)
├── searchKeywords (text, private)
├── lastUpdated (datetime)
└── seo (component: shared.seo)
Relationen:
├── category → FAQ-Category (manyToOne)
├── relatedFaqs ↔ FAQ (manyToMany, self-referential)
├── relatedServices ↔ Service (manyToMany)
├── relatedExperts ↔ Expert (manyToMany)
└── sites ↔ Site-Configuration (manyToMany)
```
#### FAQ-CATEGORY
```
Felder:
├── name (string, required, unique, max 100)
├── slug (uid, required, targetField: name)
├── description (text, max 300)
├── icon (string, default "question-circle")
├── color (string, hex, default #3B82F6)
└── order (integer, default 0)
Relationen:
├── faqs ← FAQ (oneToMany)
└── sites ↔ Site-Configuration (manyToMany)
```
#### FAQ-FEEDBACK
```
Felder:
├── isHelpful (boolean, required)
├── comment (text, max 1000)
├── additionalFeedback (enum: too_technical, not_detailed_enough, outdated, confusing, perfect, needs_examples)
├── userEmail (email)
├── ipAddress (string, private)
├── userAgent (string, private)
├── sessionId (string, private)
├── followUpRequested (boolean, default false)
├── resolved (boolean, default false)
├── resolvedAt (datetime)
└── internalNotes (text, private)
Relationen:
├── faq → FAQ (manyToOne, required)
├── site → Site-Configuration (manyToOne)
└── resolvedBy → Admin-User (manyToOne)
```
#### LEGAL-PAGE
```
Felder:
├── type (enum: impressum, datenschutz, agb, cookie-policy, required)
├── content (richtext, required)
├── country (enum: global, de, at, ch, nl, es, uk)
├── language (enum: de, en, nl, es, fr, it)
├── validFrom (date)
├── validUntil (date)
└── version (string)
Relationen: Keine
```
#### NEWS-ITEM
```
Felder:
├── title (string, required, max 150)
├── slug (uid, required, targetField: title)
├── summary (text, required, max 250)
├── content (richtext, required)
├── featuredImage (media, images only)
├── newsType (enum: announcement, update, event, press_release, medical_news)
├── priority (enum: normal, important, urgent)
├── publishDate (datetime, required)
├── expiryDate (datetime)
├── author (component: blog.author)
├── tags (json)
├── attachments (media, multiple)
├── externalLink (string)
└── seo (component: shared.seo)
Relationen:
├── sites ↔ Site-Configuration (manyToMany)
└── relatedServices ↔ Service (manyToMany)
```
#### PAGE (Dynamischer Page Builder)
```
Felder:
├── title (string, required)
├── slug (uid, required, targetField: title)
├── isSharedContent (boolean, default false)
├── sections (dynamiczone) ← KRITISCH FÜR MIGRATION
└── seo (component: shared.seo)
Dynamic Zone Components (11):
├── sections.hero
├── sections.text-block
├── sections.cta-banner
├── sections.services-grid
├── sections.team-grid
├── sections.testimonials
├── sections.expert-grid
├── sections.faq
├── sections.contact-form
├── sections.blog-list
└── sections.faq-reference
Relationen:
└── sites ↔ Site-Configuration (manyToMany)
```
#### SERVICE (Medizinische Dienstleistungen)
```
Felder:
├── name (string, required)
├── slug (uid, required, targetField: name)
├── description (richtext, required)
├── shortDescription (text, max 300)
├── icon (media, images only)
├── featuredImage (media, images only)
├── price (component: service.pricing)
├── features (component: service.feature, repeatable)
├── duration (string)
├── category (enum: diagnostic, therapeutic, preventive, surgical, emergency, consultation)
├── isActive (boolean, default true)
├── order (integer, default 0)
└── seo (component: shared.seo)
Relationen:
├── sites ↔ Site-Configuration (manyToMany)
└── experts ↔ Expert (manyToMany)
```
#### SITE-CONFIGURATION (Zentrale Domain-Konfiguration)
```
Felder:
├── siteIdentifier (string, required, unique)
├── domain (string, required)
├── siteName (string, required, max 100)
├── tagline (string, max 200)
├── logo (media, images only)
├── favicon (media, images only)
├── aliases (json)
├── brand (enum: complexcare, zweitmeinung, portal, required)
├── specialty (enum: general, intensiv, kardiologie, onkologie, chirurgie, radiologie, nephrologie, orthopaedie, polypharmazie, pflege)
├── locales (json, default: de-DE)
├── navigation (json)
├── footer (json)
├── theme (component: site.theme)
├── contact (component: site.contact)
├── features (json)
├── socialMedia (component: site.social-media)
├── analytics (component: site.analytics)
├── seo (component: shared.seo)
├── portalSettings (component: site.portal-settings)
├── maintenanceMode (component: site.maintenance-mode)
├── emailSettings (component: site.email-settings, required)
└── appointmentSettings (component: appointment.appointment-settings)
Relationen:
├── newsItems ↔ News-Item (manyToMany)
├── blogPosts ↔ Blog-Post (manyToMany)
├── services ↔ Service (manyToMany)
├── experts ↔ Expert (manyToMany)
└── pages ↔ Page (manyToMany)
```
#### WAITING-LIST
```
Felder:
├── waitingListId (UUID, unique)
├── patientName (string, required, max 100)
├── patientEmail (email, required)
├── patientPhone (string, required, max 30)
├── preferredDates (json, default [])
├── preferredTimeOfDay (enum: morning, afternoon, evening, any)
├── flexibleDates (boolean, default false)
├── priority (enum: normal, urgent, emergency)
├── status (enum: waiting, contacted, appointed, expired, cancelled)
├── notes (text, max 1000)
├── internalNotes (text, private)
├── addedAt (datetime, private)
├── contactedAt (datetime, private)
├── appointedAt (datetime, private)
├── expiresAt (datetime)
├── contactAttempts (integer, private)
├── lastContactAttempt (datetime, private)
├── patientConsent (boolean, required)
└── ipAddress (string, private)
Relationen:
├── expert → Expert (manyToOne)
├── service → Service (manyToOne)
└── site → Site-Configuration (manyToOne)
```
---
## 3. Components / Blocks
### 3.1 Component-Kategorien Übersicht
| Kategorie | Anzahl | Verwendungszweck |
|-----------|--------|------------------|
| admin | 1 | Analytics Dashboard |
| appointment | 5 | Terminverwaltung-Einstellungen |
| blog | 2 | Blog-Autoren & Kategorien |
| elements | 1 | Wiederverwendbare UI-Elemente |
| expert | 2 | Qualifikationen & Verfügbarkeit |
| layout | 3 | Navigation & Footer |
| sections | 11 | Page Builder Blöcke |
| service | 2 | Preise & Features |
| shared | 3 | SEO & Meta-Daten |
| site | 12 | Site-Konfiguration |
**Gesamt: 43 Components**
---
### 3.2 Detaillierte Component-Definitionen
#### SHARED (Wiederverwendbar in mehreren Content-Types)
**shared.seo** (Verwendet in 7 Content-Types)
```
├── metaTitle (string, max 60)
├── metaDescription (text, max 160)
├── keywords (text)
├── canonicalURL (string)
├── structuredData (json)
├── metaImage (media, images only)
├── openGraph (component: shared.open-graph)
└── metaSocial (component: shared.meta-social, repeatable)
```
**shared.open-graph**
```
├── type (enum: website, article, profile, book, music, video)
├── title (string, max 95)
├── description (text, max 200)
├── image (media, images only)
├── url (string)
├── siteName (string, max 50)
└── locale (string, default "de_DE")
```
**shared.meta-social**
```
├── socialNetwork (enum: Facebook, Twitter, LinkedIn, required)
├── title (string, max 60)
├── description (text, max 160)
└── image (media, images only)
```
---
#### ELEMENTS (Basis-Bausteine)
**elements.button** (Verwendet in 7 Components)
```
├── label (string, required)
├── url (string)
├── style (enum: primary, secondary, outline)
├── icon (string)
└── openInNewTab (boolean, default false)
```
---
#### SECTIONS (Page Builder Blöcke)
**sections.hero**
```
├── title (string)
├── subtitle (text)
├── backgroundImage (media)
├── alignment (enum: left, center, right)
└── buttons (component: elements.button, repeatable)
```
**sections.text-block**
```
├── content (richtext, required)
├── layout (enum: full-width, centered, two-columns)
└── backgroundColor (enum: white, light, dark, primary)
```
**sections.cta-banner**
```
├── title (string, required)
├── description (text)
├── button (component: elements.button)
└── backgroundStyle (enum: gradient, solid, image)
```
**sections.services-grid**
```
├── title (string)
├── subtitle (text)
├── serviceIds (json)
├── displayMode (enum: all, selected, category)
├── category (enum: diagnostic, therapeutic, preventive, surgical, emergency, consultation)
├── columns (integer, 2-4)
├── showPrices (boolean)
└── cta (component: elements.button)
```
**sections.team-grid**
```
├── title (string)
├── subtitle (text)
├── members (component: sections.team-member, repeatable)
└── columns (integer, 2-4)
```
**sections.team-member**
```
├── name (string, required)
├── role (string, required)
├── bio (text)
├── image (media, images only)
└── socialLinks (json)
```
**sections.testimonials**
```
├── title (string)
├── subtitle (text)
└── testimonials (component: sections.testimonial, repeatable)
```
**sections.testimonial**
```
├── content (text, required)
├── author (string, required)
├── role (string)
├── company (string)
├── rating (integer, 1-5)
└── image (media, images only)
```
**sections.expert-grid**
```
├── title (string)
├── subtitle (text)
├── expertIds (json)
├── displayMode (enum: all, selected, specialty)
├── specialty (string)
├── columns (integer, 2-4)
├── showAvailability (boolean)
└── cta (component: elements.button)
```
**sections.faq**
```
├── title (string)
├── subtitle (text)
└── questions (component: sections.faq-item, repeatable)
```
**sections.faq-item**
```
├── question (string, required)
└── answer (richtext, required)
```
**sections.faq-reference** (Referenziert zentrales FAQ-System)
```
├── title (string)
├── subtitle (text)
├── displayMode (enum: accordion, list, cards, compact)
├── source (enum: selected, category, featured, popular)
├── selectedFaqs (json)
├── categorySlug (string)
├── limit (integer, 1-50)
├── showSearch (boolean)
├── showCategories (boolean)
└── cta (component: elements.button)
```
**sections.blog-list**
```
├── title (string)
├── subtitle (text)
├── contentType (enum: blog_posts, news_items, both)
├── displayMode (enum: grid, list, featured)
├── itemsToShow (integer, 1-12)
├── filterByCategory (string)
├── showPagination (boolean)
└── cta (component: elements.button)
```
**sections.contact-form**
```
├── title (string)
├── subtitle (text)
├── formFields (json)
├── submitButtonText (string)
├── successMessage (text)
├── recipientEmail (email)
└── consentText (text)
```
---
#### SITE (Konfiguration)
**site.theme**
```
├── primaryColor (string, hex, default #2563eb)
├── secondaryColor (string, hex, default #10b981)
├── accentColor (string, hex, default #f59e0b)
├── darkColor (string, hex, default #1f2937)
├── backgroundColor (string, hex, default #f3f4f6)
├── fontFamily (string, default "Inter, sans-serif")
└── customCSS (text)
```
**site.contact**
```
├── email (email, required)
├── phone (string)
├── fax (string)
├── address (text)
├── emergencyHotline (string)
└── openingHours (json)
```
**site.social-media**
```
├── facebook (string, regex validated)
├── twitter (string, regex validated)
├── linkedin (string, regex validated)
├── instagram (string, regex validated)
├── youtube (string, regex validated)
├── xing (string, regex validated)
├── tiktok (string, regex validated)
├── whatsapp (string)
└── telegram (string)
```
**site.analytics**
```
├── googleAnalyticsId (string, regex: UA-* or G-*)
├── googleTagManagerId (string, regex: GTM-*)
├── facebookPixelId (string)
├── matomo (json)
├── hotjar (json)
├── clarity (string)
├── cookieConsent (json)
└── customScripts (json)
```
**site.email-settings**
```
├── fromEmail (email, required)
├── fromName (string, required)
├── replyToEmail (email, required)
├── notificationEmail (email, required)
├── ccEmails (json)
├── emailSignature (richtext)
├── emailFooter (text)
├── notificationSettings (json)
└── emailTemplateStyle (json)
```
**site.maintenance-mode**
```
├── enabled (boolean, required)
├── startDate (datetime)
├── endDate (datetime)
├── title (string)
├── message (richtext, required)
├── allowedIPs (json, private)
├── allowedRoles (json)
├── showProgressBar (boolean)
├── progressPercentage (integer, 0-100)
├── contactInfo (json)
├── customCSS (text)
└── redirectUrl (string)
```
**site.portal-settings**
```
├── isPortal (boolean, required)
├── aggregatedDomains (component: site.domain-reference, repeatable)
├── showDomainGrid (boolean)
├── showLiveStatistics (boolean)
├── showExpertCarousel (boolean)
├── enableGlobalSearch (boolean)
├── showTrustIndicators (boolean)
├── statisticsConfig (component: site.statistics-config)
├── quickAccessCategories (json)
├── featuredExperts (json)
└── highlightedServices (json)
```
**site.domain-reference**
```
├── domain (string, required)
├── siteIdentifier (string, required)
├── displayName (string)
├── category (enum)
├── order (integer)
└── isActive (boolean)
```
**site.statistics-config**
```
├── showTotalOpinions (boolean)
├── showActiveExperts (boolean)
├── showSatisfactionRate (boolean)
├── showResponseTime (boolean)
├── refreshInterval (integer)
├── animateNumbers (boolean)
└── displayFormat (enum: compact, detailed, minimal)
```
---
#### APPOINTMENT (Terminverwaltung)
**appointment.appointment-settings**
```
├── bookingAdvanceDays (integer, 1-365)
├── minBookingHours (integer, 0-168)
├── maxBookingDays (integer, 1-365)
├── cancellationHours (integer, 0-168)
├── defaultDuration (integer, 15-240)
├── workingHours (component: appointment.working-hours)
├── bookingRules (json)
├── reminderHours (integer, 0-168)
├── allowWaitingList (boolean)
├── requireConfirmation (boolean)
├── requirePayment (boolean)
├── autoConfirmHours (integer)
├── maxAppointmentsPerDay (integer)
├── bufferBetweenAppointments (integer)
├── allowOnlineBooking (boolean)
├── showPrices (boolean)
├── termsAndConditions (richtext)
├── emailTemplates (component: appointment.email-templates)
├── smsEnabled (boolean)
└── notificationChannels (json)
```
**appointment.working-hours**
```
├── monday (component: appointment.day-schedule)
├── tuesday (component: appointment.day-schedule)
├── wednesday (component: appointment.day-schedule)
├── thursday (component: appointment.day-schedule)
├── friday (component: appointment.day-schedule)
├── saturday (component: appointment.day-schedule)
└── sunday (component: appointment.day-schedule)
```
**appointment.day-schedule**
```
├── isOpen (boolean)
├── openTime (time)
├── closeTime (time)
└── breaks (component: appointment.break-time, repeatable)
```
**appointment.break-time**
```
├── startTime (time, required)
├── endTime (time, required)
└── description (string)
```
**appointment.email-templates**
```
├── bookingConfirmation (richtext)
├── appointmentReminder (richtext)
├── cancellationConfirmation (richtext)
├── waitingListNotification (richtext)
├── noShowFollowUp (richtext)
├── appointmentRescheduled (richtext)
├── senderName (string)
├── senderEmail (email, required)
└── replyToEmail (email)
```
---
#### EXPERT
**expert.qualification**
```
├── title (string, required)
├── institution (string)
├── year (integer)
├── description (text)
└── type (enum: degree, certification, specialization, award, membership)
```
**expert.availability**
```
├── schedule (json)
├── consultationTypes (json)
├── locations (json)
├── bookingUrl (string)
├── responseTime (string)
└── languages (json)
```
---
#### BLOG
**blog.author**
```
├── name (string, required)
├── bio (text, max 500)
├── avatar (media, images only)
├── role (string)
└── socialLinks (json)
```
---
#### SERVICE
**service.pricing**
```
├── basePrice (decimal, required)
├── currency (string, default EUR)
├── priceNote (text)
├── isStartingPrice (boolean)
└── paymentOptions (json)
```
**service.feature**
```
├── title (string, required)
├── description (text)
└── icon (string)
```
---
## 4. API-Struktur
### 4.1 API-Endpoints Übersicht
| Modul | Custom Logic | Endpoints | Authentifizierung |
|-------|--------------|-----------|-------------------|
| appointment | Ja (14 Methods) | 13 | Nein (Token-basiert) |
| appointment-exception | Nein | 5 | Nein |
| appointment-slot | Nein | 5 | Nein |
| blog-category | Ja (1 Method) | 6 | Nein |
| blog-post | Ja (2 Methods) | 7 | Optional |
| contact-message | Ja (4 Methods) | 8 | Nein |
| expert | Nein | 5 | Optional |
| faq | Nein | 5 | Nein |
| faq-category | Nein | 5 | Nein |
| faq-feedback | Nein | 5 | Nein |
| legal-page | Nein | 5 | Optional |
| news-item | Nein | 5 | Optional |
| page | Nein | 5 | Optional |
| service | Nein | 5 | Optional |
| site-configuration | Nein | 5 | Optional |
| test-email | Ja (2 Methods) | 2 | Nein |
| waiting-list | Nein | 5 | Nein |
---
### 4.2 Custom API-Logik (Migration erforderlich)
#### Appointment Controller (14 Custom Methods)
```typescript
// Kritische Business Logic für Payload Migration
1. getAvailability(expert, service, startDate, endDate)
- Berechnet verfügbare Zeitslots
- Berücksichtigt: Slots, Exceptions, bestehende Termine
- Filtert nach Buchungsfenster (24h-90d)
2. bookAppointment(patientData, appointmentDetails)
- Validiert alle Felder
- Prüft Slot-Verfügbarkeit
- Generiert Confirmation/Cancellation Tokens
- Sendet Bestätigungs-Email
3. confirmAppointment(token)
- Validiert Token
- Setzt Status auf 'confirmed'
- Sendet Bestätigungs-Email
4. cancelAppointment(token, reason)
- Prüft Stornierungsrichtlinie (24h)
- Setzt Status auf 'cancelled'
- Verarbeitet Warteliste
- Benachrichtigt nächsten Patienten
5. rescheduleAppointment(appointmentId, newSlot)
- Validiert neuen Slot
- Aktualisiert Termin
- Sendet Benachrichtigung
6. addToWaitingList(patientData, preferences)
- Erstellt Wartelisten-Eintrag
- Sendet Bestätigung
7. getCalendarView(expert, view, date)
- Erstellt Kalender-Übersicht
- Gruppiert nach Tag/Woche/Monat
8. getStatistics(period, expert)
- Berechnet Statistiken
- Status-Verteilung, No-Show-Rate, etc.
9. sendReminders()
- Daily Job für morgige Termine
- Setzt reminderSent Flag
10. exportAppointments(format, filters)
- CSV-Export
- Gefiltert nach Datum/Expert/Status
11-14. Email Services
- sendConfirmationEmail()
- sendCancellationEmail()
- sendReminderEmail()
- sendWaitingListNotification()
```
#### Contact-Message Controller (4 Custom Methods)
```typescript
1. create(data) - mit Spam-Schutz
- Honeypot-Feld Prüfung
- Minimum 3 Sekunden Ausfüllzeit
- Email-Benachrichtigung an Site-Admin
2. markAsRead(id)
- Setzt Status auf 'read'
3. markAsSpam(id)
- Setzt Status auf 'spam'
4. stats()
- Statistiken der letzten 30 Tage
```
#### Blog-Post Controller (2 Custom Methods)
```typescript
1. findRelated(postId)
- Findet ähnliche Posts nach Kategorie
2. findByCategory(categorySlug, pagination)
- Filtert Posts nach Kategorie-Slug
```
---
### 4.3 Middleware
**Contact Rate Limiting** (Optional)
```typescript
// /src/middlewares/contact-rate-limit.ts
{
limit: 5, // Anfragen
interval: 60, // Sekunden
tracking: 'ip+path'
}
```
---
## 5. Plugins & Extensions
### 5.1 Installierte Plugins
| Plugin | Version | Payload Äquivalent |
|--------|---------|-------------------|
| @strapi/plugin-documentation | 5.18.0 | Built-in API Docs |
| @strapi/plugin-graphql | 5.18.0 | @payloadcms/plugin-graphql |
| @strapi/plugin-users-permissions | 5.18.0 | Built-in Auth |
| @strapi/plugin-seo | 2.0.8 | @payloadcms/plugin-seo |
| @strapi/provider-email-nodemailer | 5.18.0 | Nodemailer via Hooks |
| @_sh/strapi-plugin-ckeditor | 6.0.2 | Lexical Editor (built-in) |
| strapi-plugin-navigation | 3.0.16 | @payloadcms/plugin-nested-docs |
| strapi-plugin-superfields | 5.8.2 | Custom Field Types |
| strapi-plugin-multi-select | 2.1.1 | Select Field (built-in) |
| strapi-plugin-country-select | 2.1.0 | Custom Select |
| strapi-plugin-bold-title-editor | 2.0.0 | Custom RichText Config |
| strapi-5-sitemap-plugin | 1.0.7 | afterChange Hook |
| strapi-cache | 1.5.5 | Redis Plugin |
| strapi-advanced-uuid | 2.1.1 | Custom ID Field |
| strapi-5-plugin-responsive-backend | 0.0.4 | Built-in Responsive UI |
| @chartbrew/plugin-strapi | 3.0.0 | External Integration |
### 5.2 Plugin-Konfigurationen
**CKEditor 5 Toolbar:**
```
Heading | Bold, Italic, Underline, Strikethrough |
BulletedList, NumberedList | Blockquote |
Table | Code, CodeBlock | Link | Image
```
**Navigation Plugin:**
```typescript
{
contentTypes: ["page", "service", "expert", "blog-post", "faq"],
allowedLevels: 3
}
```
**Cache Plugin:**
```typescript
{
type: "mem",
maxAge: 3600000,
cacheRoutes: ["/api/services", "/api/experts", "/api/faq", "/api/blog-posts"]
}
```
---
## 6. Datenbank & Relationen
### 6.1 Relationen-Diagramm
```
┌─────────────────────────────────────────────────────────────────┐
│ SITE-CONFIGURATION │
│ (Zentrale Konfiguration) │
└─────────────────────────────────────────────────────────────────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ Expert │ │Service │ │ Page │ │BlogPost│ │NewsItem│
└────────┘ └────────┘ └────────┘ └────────┘ └────────┘
│ │ │
│ │ ▼
│ │ ┌─────────────┐
│ │ │BlogCategory │
│ │ └─────────────┘
│ │
▼ ▼
┌─────────────────────────────────────────┐
│ APPOINTMENT │
│ (Termin-Buchungen) │
└─────────────────────────────────────────┘
├──────────────────┐
▼ ▼
┌───────────────┐ ┌─────────────────────┐
│Appointment- │ │ Appointment- │
│Exception │ │ Slot │
└───────────────┘ └─────────────────────┘
┌─────────────────────────────────────────┐
│ FAQ SYSTEM │
└─────────────────────────────────────────┘
┌────────────┐ ┌────────┐ ┌─────────────┐
│FAQ-Category│ ──► │ FAQ │ ◄── │FAQ-Feedback │
└────────────┘ └────────┘ └─────────────┘
┌─────────────────────┐
│ Related: Service, │
│ Expert, FAQ │
└─────────────────────┘
┌─────────────────────────────────────────┐
│ CONTACT & WAITING │
└─────────────────────────────────────────┘
┌────────────────┐ ┌─────────────┐
│Contact-Message │ │Waiting-List │
│ → Site │ │→Expert,Svc │
└────────────────┘ └─────────────┘
```
### 6.2 Bidirektionale Relationen (ManyToMany)
| Relation | Seite A | Seite B |
|----------|---------|---------|
| 1 | Site-Configuration | Expert |
| 2 | Site-Configuration | Service |
| 3 | Site-Configuration | Page |
| 4 | Site-Configuration | Blog-Post |
| 5 | Site-Configuration | News-Item |
| 6 | Site-Configuration | FAQ |
| 7 | Site-Configuration | FAQ-Category |
| 8 | Expert | Service |
| 9 | FAQ | FAQ (self-referential) |
| 10 | FAQ | Service |
| 11 | FAQ | Expert |
| 12 | Blog-Category | Blog-Category (self-referential) |
---
## 7. Migrationsplan
### 7.1 Phase 1: Payload CMS Setup (Woche 1)
```bash
# Installation
npx create-payload-app@latest zweitmeinu-ng-payload
cd zweitmeinu-ng-payload
# Plugins
npm install @payloadcms/plugin-graphql
npm install @payloadcms/plugin-seo
npm install @payloadcms/plugin-nested-docs
npm install @payloadcms/plugin-form-builder
npm install @payloadcms/richtext-lexical
```
### 7.2 Phase 2: Collections erstellen (Woche 2-3)
**Priorität 1 - Basis Collections:**
1. Users (Admin)
2. Media
3. Site-Configuration → Sites
4. Legal-Page → LegalPages
**Priorität 2 - Content Collections:**
5. Expert → Experts
6. Service → Services
7. FAQ-Category → FAQCategories
8. FAQ → FAQs
9. Blog-Category → BlogCategories
10. Blog-Post → BlogPosts
11. News-Item → NewsItems
**Priorität 3 - Page Builder:**
12. Page → Pages (mit Blocks)
**Priorität 4 - Appointment System:**
13. Appointment → Appointments
14. Appointment-Slot → AppointmentSlots
15. Appointment-Exception → AppointmentExceptions
16. Waiting-List → WaitingList
**Priorität 5 - Formulare:**
17. Contact-Message → ContactMessages
18. FAQ-Feedback → FAQFeedback
### 7.3 Phase 3: Blocks erstellen (Woche 3)
```typescript
// /src/blocks/index.ts
export const blocks = [
HeroBlock,
TextBlock,
CTABanner,
ServicesGrid,
TeamGrid,
Testimonials,
ExpertGrid,
FAQSection,
FAQReference,
BlogList,
ContactForm,
];
```
### 7.4 Phase 4: Custom Endpoints (Woche 4)
```typescript
// /src/endpoints/appointments.ts
export const appointmentEndpoints = {
getAvailability,
bookAppointment,
confirmAppointment,
cancelAppointment,
rescheduleAppointment,
getCalendarView,
getStatistics,
sendReminders,
exportAppointments,
};
```
### 7.5 Phase 5: Daten-Migration (Woche 5)
```typescript
// Migration Script Struktur
const migrationOrder = [
'site-configuration',
'expert',
'service',
'faq-category',
'faq',
'blog-category',
'blog-post',
'news-item',
'legal-page',
'page',
'appointment-slot',
'appointment-exception',
'appointment',
'waiting-list',
'contact-message',
'faq-feedback',
];
```
### 7.6 Phase 6: Testing & Go-Live (Woche 6)
1. API-Kompatibilitätstests
2. Frontend-Integration
3. Performance-Tests
4. Staging-Deployment
5. Production-Migration
---
## 8. Payload CMS Schema-Entwürfe
### 8.1 Beispiel: Appointments Collection
```typescript
// /src/collections/Appointments.ts
import { CollectionConfig } from 'payload/types';
export const Appointments: CollectionConfig = {
slug: 'appointments',
admin: {
useAsTitle: 'patientName',
defaultColumns: ['patientName', 'appointmentDate', 'status', 'expert'],
group: 'Termine',
},
access: {
read: () => true,
create: () => true,
update: ({ req: { user } }) => Boolean(user),
delete: ({ req: { user } }) => Boolean(user),
},
fields: [
{
name: 'appointmentId',
type: 'text',
unique: true,
admin: { readOnly: true },
hooks: {
beforeChange: [({ value }) => value || crypto.randomUUID()],
},
},
{
type: 'row',
fields: [
{ name: 'patientName', type: 'text', required: true, maxLength: 100 },
{ name: 'patientEmail', type: 'email', required: true },
{ name: 'patientPhone', type: 'text', required: true, maxLength: 30 },
],
},
{
type: 'row',
fields: [
{ name: 'appointmentDate', type: 'date', required: true },
{ name: 'appointmentTime', type: 'text', required: true }, // HH:MM format
{ name: 'duration', type: 'number', min: 15, max: 240, defaultValue: 30 },
],
},
{
name: 'appointmentType',
type: 'select',
required: true,
options: [
{ label: 'Beratung', value: 'consultation' },
{ label: 'Untersuchung', value: 'examination' },
{ label: 'Behandlung', value: 'treatment' },
{ label: 'Nachsorge', value: 'followup' },
{ label: 'Impfung', value: 'vaccination' },
{ label: 'Sonstiges', value: 'other' },
],
},
{
name: 'status',
type: 'select',
required: true,
defaultValue: 'requested',
options: [
{ label: 'Angefragt', value: 'requested' },
{ label: 'Bestätigt', value: 'confirmed' },
{ label: 'Storniert', value: 'cancelled' },
{ label: 'Abgeschlossen', value: 'completed' },
{ label: 'Nicht erschienen', value: 'no-show' },
],
},
{
name: 'expert',
type: 'relationship',
relationTo: 'experts',
},
{
name: 'service',
type: 'relationship',
relationTo: 'services',
},
{
name: 'site',
type: 'relationship',
relationTo: 'sites',
},
{
name: 'tokens',
type: 'group',
admin: { condition: () => false }, // Hidden in admin
fields: [
{ name: 'confirmationToken', type: 'text' },
{ name: 'cancellationToken', type: 'text' },
],
},
{
name: 'notes',
type: 'textarea',
maxLength: 1000,
},
{
name: 'internalNotes',
type: 'textarea',
admin: { condition: ({ req: { user } }) => Boolean(user) },
},
{
name: 'patientConsent',
type: 'checkbox',
required: true,
},
{
name: 'timestamps',
type: 'group',
admin: { readOnly: true },
fields: [
{ name: 'requestedAt', type: 'date' },
{ name: 'confirmedAt', type: 'date' },
{ name: 'cancelledAt', type: 'date' },
{ name: 'completedAt', type: 'date' },
],
},
],
hooks: {
beforeChange: [
async ({ data, operation }) => {
if (operation === 'create') {
data.timestamps = { requestedAt: new Date() };
data.tokens = {
confirmationToken: crypto.randomUUID(),
cancellationToken: crypto.randomUUID(),
};
}
return data;
},
],
afterChange: [
async ({ doc, operation }) => {
if (operation === 'create') {
// Send confirmation email
await sendAppointmentConfirmationEmail(doc);
}
},
],
},
};
```
### 8.2 Beispiel: Page Collection mit Blocks
```typescript
// /src/collections/Pages.ts
import { CollectionConfig } from 'payload/types';
import { blocks } from '../blocks';
export const Pages: CollectionConfig = {
slug: 'pages',
admin: {
useAsTitle: 'title',
group: 'Content',
},
versions: {
drafts: true,
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
admin: {
position: 'sidebar',
},
hooks: {
beforeValidate: [({ value, data }) =>
value || data?.title?.toLowerCase().replace(/\s+/g, '-')
],
},
},
{
name: 'isSharedContent',
type: 'checkbox',
defaultValue: false,
admin: { position: 'sidebar' },
},
{
name: 'sections',
type: 'blocks',
blocks,
},
{
name: 'sites',
type: 'relationship',
relationTo: 'sites',
hasMany: true,
admin: { position: 'sidebar' },
},
{
name: 'seo',
type: 'group',
fields: [
{ name: 'metaTitle', type: 'text', maxLength: 60 },
{ name: 'metaDescription', type: 'textarea', maxLength: 160 },
{ name: 'metaImage', type: 'upload', relationTo: 'media' },
],
},
],
};
```
### 8.3 Beispiel: Hero Block
```typescript
// /src/blocks/Hero.ts
import { Block } from 'payload/types';
export const HeroBlock: Block = {
slug: 'hero',
labels: {
singular: 'Hero',
plural: 'Heroes',
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'subtitle',
type: 'textarea',
},
{
name: 'backgroundImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'alignment',
type: 'select',
defaultValue: 'center',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Zentriert', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'buttons',
type: 'array',
fields: [
{ name: 'label', type: 'text', required: true },
{ name: 'url', type: 'text' },
{
name: 'style',
type: 'select',
options: ['primary', 'secondary', 'outline'],
},
{ name: 'openInNewTab', type: 'checkbox' },
],
},
],
};
```
---
## Anhang: Checkliste für Migration
### Content-Types → Collections
- [ ] appointment → Appointments
- [ ] appointment-exception → AppointmentExceptions
- [ ] appointment-slot → AppointmentSlots
- [ ] blog-category → BlogCategories
- [ ] blog-post → BlogPosts
- [ ] contact-message → ContactMessages
- [ ] expert → Experts
- [ ] faq → FAQs
- [ ] faq-category → FAQCategories
- [ ] faq-feedback → FAQFeedback
- [ ] legal-page → LegalPages
- [ ] news-item → NewsItems
- [ ] page → Pages
- [ ] service → Services
- [ ] site-configuration → Sites
- [ ] waiting-list → WaitingList
### Components → Blocks/Fields
- [ ] shared.seo → SEO Group Field
- [ ] elements.button → Button Array Field
- [ ] sections.hero → HeroBlock
- [ ] sections.text-block → TextBlock
- [ ] sections.cta-banner → CTABannerBlock
- [ ] sections.services-grid → ServicesGridBlock
- [ ] sections.team-grid → TeamGridBlock
- [ ] sections.testimonials → TestimonialsBlock
- [ ] sections.expert-grid → ExpertGridBlock
- [ ] sections.faq → FAQBlock
- [ ] sections.faq-reference → FAQReferenceBlock
- [ ] sections.blog-list → BlogListBlock
- [ ] sections.contact-form → ContactFormBlock
- [ ] site.theme → ThemeGroup
- [ ] site.contact → ContactGroup
- [ ] site.social-media → SocialMediaGroup
- [ ] site.analytics → AnalyticsGroup
- [ ] site.email-settings → EmailSettingsGroup
- [ ] appointment.appointment-settings → AppointmentSettingsGroup
- [ ] appointment.working-hours → WorkingHoursGroup
- [ ] appointment.email-templates → EmailTemplatesGroup
### Custom API Logic → Hooks/Endpoints
- [ ] Appointment Availability Calculation
- [ ] Appointment Booking Flow
- [ ] Appointment Confirmation (Token-based)
- [ ] Appointment Cancellation
- [ ] Waiting List Processing
- [ ] Email Notifications
- [ ] Statistics Calculation
- [ ] CSV Export
- [ ] Contact Form Spam Protection
- [ ] Blog Related Posts
### Plugins → Payload Äquivalente
- [ ] GraphQL Plugin
- [ ] SEO Plugin
- [ ] Nested Docs (Navigation)
- [ ] Rich Text Editor Configuration
- [ ] Caching Strategy
- [ ] Sitemap Generation
---
*Dokument generiert am 2026-02-20*