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

45 KiB

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
  2. Content-Types / Collections
  3. Components / Blocks
  4. API-Struktur
  5. Plugins & Extensions
  6. Datenbank & Relationen
  7. Migrationsplan
  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)
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)

// 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)

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)

1. findRelated(postId)
   - Findet ähnliche Posts nach Kategorie

2. findByCategory(categorySlug, pagination)
   - Filtert Posts nach Kategorie-Slug

4.3 Middleware

Contact Rate Limiting (Optional)

// /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:

{
  contentTypes: ["page", "service", "expert", "blog-post", "faq"],
  allowedLevels: 3
}

Cache Plugin:

{
  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)

# 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)

// /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)

// /src/endpoints/appointments.ts
export const appointmentEndpoints = {
  getAvailability,
  bookAppointment,
  confirmAppointment,
  cancelAppointment,
  rescheduleAppointment,
  getCalendarView,
  getStatistics,
  sendReminders,
  exportAppointments,
};

7.5 Phase 5: Daten-Migration (Woche 5)

// 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

// /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

// /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

// /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