cms.c2sgmbh/docs/PROMPT_UNIVERSAL_FEATURES_PAYLOAD.md
Martin Porwoll a88e4f60d0 test: add E2E and integration tests with documentation
Tests:
- Update frontend.e2e.spec.ts with locale testing
- Add search.e2e.spec.ts for search functionality
- Add i18n.int.spec.ts for localization tests
- Add search.int.spec.ts for search integration
- Update playwright.config.ts

Documentation:
- Add CLAUDE.md with project instructions
- Add docs/ directory with detailed documentation
- Add scripts/ for utility scripts

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

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

33 KiB

PROMPT: Universelle Features - Payload CMS

Kontext

Du arbeitest auf dem Server sv-payload (10.10.181.100) im Verzeichnis /home/payload/payload-cms.

Diese Erweiterungen sind für alle Tenants nutzbar und bilden die Grundlage für Blog, News, Testimonials, Newsletter und Prozess-Darstellungen.

Übersicht

COLLECTIONS (3)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✏️  posts           → ERWEITERN (type, isFeatured, excerpt)
🆕 testimonials    → NEU
🆕 newsletter-subscribers → NEU

BLOCKS (5)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
🆕 posts-list-block
🆕 testimonials-block
🆕 newsletter-block
🆕 process-steps-block
✏️  timeline-block  → ERWEITERN

TEIL 1: Collections

1.1 Posts Collection erweitern

Bearbeite src/collections/Posts.ts und füge folgende Felder hinzu:

// src/collections/Posts.ts

import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'

export const Posts: CollectionConfig = {
  slug: 'posts',
  admin: {
    useAsTitle: 'title',
    group: 'Content',
    defaultColumns: ['title', 'type', 'isFeatured', 'status', 'publishedAt', 'tenant'],
  },
  access: {
    read: tenantScopedPublicRead,
    create: authenticatedOnly,
    update: authenticatedOnly,
    delete: authenticatedOnly,
  },
  fields: [
    {
      name: 'tenant',
      type: 'relationship',
      relationTo: 'tenants',
      required: true,
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'title',
      type: 'text',
      required: true,
    },
    {
      name: 'slug',
      type: 'text',
      required: true,
      unique: true,
      admin: {
        description: 'URL-Pfad (z.B. "mein-erster-beitrag")',
      },
    },
    // === NEUE FELDER ===
    {
      name: 'type',
      type: 'select',
      required: true,
      defaultValue: 'blog',
      options: [
        { label: 'Blog-Artikel', value: 'blog' },
        { label: 'News/Aktuelles', value: 'news' },
        { label: 'Pressemitteilung', value: 'press' },
        { label: 'Ankündigung', value: 'announcement' },
      ],
      admin: {
        position: 'sidebar',
        description: 'Art des Beitrags',
      },
    },
    {
      name: 'isFeatured',
      type: 'checkbox',
      defaultValue: false,
      label: 'Hervorgehoben',
      admin: {
        position: 'sidebar',
        description: 'Auf Startseite/oben anzeigen',
      },
    },
    {
      name: 'excerpt',
      type: 'textarea',
      label: 'Kurzfassung',
      maxLength: 300,
      admin: {
        description: 'Für Übersichten und SEO (max. 300 Zeichen). Wird automatisch aus Content generiert, falls leer.',
      },
    },
    // === ENDE NEUE FELDER ===
    {
      name: 'featuredImage',
      type: 'upload',
      relationTo: 'media',
      label: 'Beitragsbild',
    },
    {
      name: 'content',
      type: 'richText',
      required: true,
    },
    {
      name: 'categories',
      type: 'relationship',
      relationTo: 'categories',
      hasMany: true,
    },
    {
      name: 'author',
      type: 'text',
      label: 'Autor',
    },
    {
      name: 'status',
      type: 'select',
      defaultValue: 'draft',
      options: [
        { label: 'Entwurf', value: 'draft' },
        { label: 'Veröffentlicht', value: 'published' },
        { label: 'Archiviert', value: 'archived' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'publishedAt',
      type: 'date',
      label: 'Veröffentlichungsdatum',
      admin: {
        position: 'sidebar',
        date: {
          pickerAppearance: 'dayAndTime',
        },
      },
    },
    {
      name: 'seo',
      type: 'group',
      label: 'SEO',
      fields: [
        {
          name: 'metaTitle',
          type: 'text',
          label: 'Meta-Titel',
        },
        {
          name: 'metaDescription',
          type: 'textarea',
          label: 'Meta-Beschreibung',
          maxLength: 160,
        },
        {
          name: 'ogImage',
          type: 'upload',
          relationTo: 'media',
          label: 'Social Media Bild',
        },
      ],
    },
  ],
}

1.2 Testimonials Collection erstellen

Erstelle src/collections/Testimonials.ts:

// src/collections/Testimonials.ts

import type { CollectionConfig } from 'payload'
import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess'

/**
 * Testimonials Collection
 * 
 * Kundenbewertungen und Referenzen, wiederverwendbar auf allen Seiten.
 * Tenant-scoped für Multi-Tenant-Betrieb.
 */
export const Testimonials: CollectionConfig = {
  slug: 'testimonials',
  admin: {
    useAsTitle: 'author',
    group: 'Content',
    defaultColumns: ['author', 'company', 'rating', 'isActive', 'tenant'],
    description: 'Kundenstimmen und Bewertungen',
  },
  access: {
    read: tenantScopedPublicRead,
    create: authenticatedOnly,
    update: authenticatedOnly,
    delete: authenticatedOnly,
  },
  fields: [
    {
      name: 'tenant',
      type: 'relationship',
      relationTo: 'tenants',
      required: true,
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'quote',
      type: 'textarea',
      required: true,
      label: 'Zitat/Bewertung',
      admin: {
        description: 'Die Aussage des Kunden',
      },
    },
    {
      name: 'author',
      type: 'text',
      required: true,
      label: 'Name',
    },
    {
      name: 'role',
      type: 'text',
      label: 'Position/Rolle',
      admin: {
        description: 'z.B. "Patient", "Geschäftsführer", "Marketing Manager"',
      },
    },
    {
      name: 'company',
      type: 'text',
      label: 'Unternehmen/Organisation',
    },
    {
      name: 'image',
      type: 'upload',
      relationTo: 'media',
      label: 'Foto',
      admin: {
        description: 'Portrait-Foto (empfohlen: quadratisch, min. 200x200px)',
      },
    },
    {
      name: 'rating',
      type: 'number',
      min: 1,
      max: 5,
      label: 'Bewertung (1-5 Sterne)',
      admin: {
        description: 'Optional: Sterne-Bewertung',
      },
    },
    {
      name: 'source',
      type: 'text',
      label: 'Quelle',
      admin: {
        description: 'z.B. "Google Reviews", "Trustpilot", "Persönlich"',
      },
    },
    {
      name: 'sourceUrl',
      type: 'text',
      label: 'Link zur Quelle',
      admin: {
        description: 'URL zur Original-Bewertung (falls öffentlich)',
      },
    },
    {
      name: 'date',
      type: 'date',
      label: 'Datum der Bewertung',
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'isActive',
      type: 'checkbox',
      defaultValue: true,
      label: 'Aktiv/Sichtbar',
      admin: {
        position: 'sidebar',
        description: 'Inaktive Testimonials werden nicht angezeigt',
      },
    },
    {
      name: 'order',
      type: 'number',
      defaultValue: 0,
      label: 'Sortierung',
      admin: {
        position: 'sidebar',
        description: 'Niedrigere Zahlen werden zuerst angezeigt',
      },
    },
  ],
}

1.3 Newsletter Subscribers Collection erstellen

Erstelle src/collections/NewsletterSubscribers.ts:

// src/collections/NewsletterSubscribers.ts

import type { CollectionConfig } from 'payload'
import { authenticatedOnly } from '../lib/tenantAccess'

/**
 * Newsletter Subscribers Collection
 * 
 * DSGVO-konforme Speicherung von Newsletter-Anmeldungen.
 * Öffentlich schreibbar (Anmeldung), nur für Admins lesbar.
 */
export const NewsletterSubscribers: CollectionConfig = {
  slug: 'newsletter-subscribers',
  admin: {
    useAsTitle: 'email',
    group: 'Marketing',
    defaultColumns: ['email', 'status', 'source', 'subscribedAt', 'tenant'],
    description: 'Newsletter-Abonnenten (DSGVO-konform)',
  },
  access: {
    // Nur Admins können Subscribers lesen (Datenschutz)
    read: authenticatedOnly,
    // Öffentlich subscriben möglich
    create: () => true,
    update: authenticatedOnly,
    delete: authenticatedOnly,
  },
  fields: [
    {
      name: 'tenant',
      type: 'relationship',
      relationTo: 'tenants',
      required: true,
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'email',
      type: 'email',
      required: true,
      label: 'E-Mail-Adresse',
    },
    {
      name: 'firstName',
      type: 'text',
      label: 'Vorname',
    },
    {
      name: 'lastName',
      type: 'text',
      label: 'Nachname',
    },
    {
      name: 'status',
      type: 'select',
      required: true,
      defaultValue: 'pending',
      options: [
        { label: 'Ausstehend (Double Opt-In)', value: 'pending' },
        { label: 'Bestätigt', value: 'confirmed' },
        { label: 'Abgemeldet', value: 'unsubscribed' },
        { label: 'Bounced', value: 'bounced' },
      ],
      admin: {
        position: 'sidebar',
      },
    },
    {
      name: 'interests',
      type: 'select',
      hasMany: true,
      label: 'Interessen',
      options: [
        { label: 'Allgemeine Updates', value: 'general' },
        { label: 'Blog-Artikel', value: 'blog' },
        { label: 'Produkt-News', value: 'products' },
        { label: 'Angebote & Aktionen', value: 'offers' },
        { label: 'Events', value: 'events' },
      ],
    },
    {
      name: 'source',
      type: 'text',
      label: 'Anmeldequelle',
      admin: {
        description: 'z.B. "Footer", "Popup", "Blog-Artikel", "Kontakt-Seite"',
      },
    },
    {
      name: 'subscribedAt',
      type: 'date',
      label: 'Anmeldedatum',
      admin: {
        readOnly: true,
        date: { pickerAppearance: 'dayAndTime' },
      },
    },
    {
      name: 'confirmedAt',
      type: 'date',
      label: 'Bestätigungsdatum',
      admin: {
        readOnly: true,
        date: { pickerAppearance: 'dayAndTime' },
      },
    },
    {
      name: 'unsubscribedAt',
      type: 'date',
      label: 'Abmeldedatum',
      admin: {
        readOnly: true,
        date: { pickerAppearance: 'dayAndTime' },
      },
    },
    {
      name: 'confirmationToken',
      type: 'text',
      label: 'Bestätigungs-Token',
      admin: {
        readOnly: true,
        hidden: true,
      },
    },
    {
      name: 'ipAddress',
      type: 'text',
      label: 'IP-Adresse',
      admin: {
        readOnly: true,
        description: 'DSGVO-Nachweis der Anmeldung',
      },
    },
    {
      name: 'userAgent',
      type: 'text',
      label: 'User Agent',
      admin: {
        readOnly: true,
        hidden: true,
      },
    },
  ],
  hooks: {
    beforeChange: [
      ({ data, operation }) => {
        // Automatisch Timestamps setzen
        if (operation === 'create') {
          data.subscribedAt = new Date().toISOString()
          // Zufälliges Token für Double Opt-In
          data.confirmationToken = crypto.randomUUID()
        }
        
        // Status-Änderungen tracken
        if (data.status === 'confirmed' && !data.confirmedAt) {
          data.confirmedAt = new Date().toISOString()
        }
        if (data.status === 'unsubscribed' && !data.unsubscribedAt) {
          data.unsubscribedAt = new Date().toISOString()
        }
        
        return data
      },
    ],
  },
  // Index für schnelle Suche
  indexes: [
    {
      fields: { email: 1, tenant: 1 },
      unique: true,
    },
  ],
}

TEIL 2: Blocks definieren

2.1 Block-Definitionen für Pages Collection

Erstelle src/blocks/index.ts (oder erweitere bestehende Datei):

// src/blocks/index.ts

import type { Block } from 'payload'

/**
 * Posts List Block
 * Zeigt Blog-Artikel, News oder andere Post-Typen an
 */
export const PostsListBlock: Block = {
  slug: 'posts-list-block',
  labels: {
    singular: 'Blog/News Liste',
    plural: 'Blog/News Listen',
  },
  imageURL: '/assets/blocks/posts-list.png',
  fields: [
    {
      name: 'title',
      type: 'text',
      label: 'Überschrift',
    },
    {
      name: 'subtitle',
      type: 'text',
      label: 'Untertitel',
    },
    {
      name: 'postType',
      type: 'select',
      required: true,
      defaultValue: 'blog',
      label: 'Beitragstyp',
      options: [
        { label: 'Blog-Artikel', value: 'blog' },
        { label: 'News/Aktuelles', value: 'news' },
        { label: 'Pressemitteilungen', value: 'press' },
        { label: 'Ankündigungen', value: 'announcement' },
        { label: 'Alle Beiträge', value: 'all' },
      ],
    },
    {
      name: 'layout',
      type: 'select',
      defaultValue: 'grid',
      label: 'Layout',
      options: [
        { label: 'Grid (Karten)', value: 'grid' },
        { label: 'Liste', value: 'list' },
        { label: 'Featured + Grid', value: 'featured' },
        { label: 'Kompakt (Sidebar)', value: 'compact' },
        { label: 'Masonry', value: 'masonry' },
      ],
    },
    {
      name: 'columns',
      type: 'select',
      defaultValue: '3',
      label: 'Spalten',
      options: [
        { label: '2 Spalten', value: '2' },
        { label: '3 Spalten', value: '3' },
        { label: '4 Spalten', value: '4' },
      ],
      admin: {
        condition: (data, siblingData) => 
          siblingData?.layout === 'grid' || 
          siblingData?.layout === 'featured' ||
          siblingData?.layout === 'masonry',
      },
    },
    {
      name: 'limit',
      type: 'number',
      defaultValue: 6,
      min: 1,
      max: 24,
      label: 'Anzahl Beiträge',
    },
    {
      name: 'showFeaturedOnly',
      type: 'checkbox',
      defaultValue: false,
      label: 'Nur hervorgehobene anzeigen',
    },
    {
      name: 'filterByCategory',
      type: 'relationship',
      relationTo: 'categories',
      hasMany: true,
      label: 'Nach Kategorien filtern',
      admin: {
        description: 'Leer = alle Kategorien',
      },
    },
    {
      name: 'showExcerpt',
      type: 'checkbox',
      defaultValue: true,
      label: 'Kurzfassung anzeigen',
    },
    {
      name: 'showDate',
      type: 'checkbox',
      defaultValue: true,
      label: 'Datum anzeigen',
    },
    {
      name: 'showAuthor',
      type: 'checkbox',
      defaultValue: false,
      label: 'Autor anzeigen',
    },
    {
      name: 'showCategory',
      type: 'checkbox',
      defaultValue: true,
      label: 'Kategorie anzeigen',
    },
    {
      name: 'showPagination',
      type: 'checkbox',
      defaultValue: false,
      label: 'Pagination anzeigen',
    },
    {
      name: 'showReadMore',
      type: 'checkbox',
      defaultValue: true,
      label: '"Alle anzeigen" Link',
    },
    {
      name: 'readMoreLabel',
      type: 'text',
      defaultValue: 'Alle Beiträge anzeigen',
      admin: {
        condition: (data, siblingData) => siblingData?.showReadMore,
      },
    },
    {
      name: 'readMoreLink',
      type: 'text',
      defaultValue: '/blog',
      admin: {
        condition: (data, siblingData) => siblingData?.showReadMore,
      },
    },
    {
      name: 'backgroundColor',
      type: 'select',
      defaultValue: 'white',
      label: 'Hintergrund',
      options: [
        { label: 'Weiß', value: 'white' },
        { label: 'Hell (Grau)', value: 'light' },
        { label: 'Dunkel', value: 'dark' },
      ],
    },
  ],
}

/**
 * Testimonials Block
 * Zeigt Kundenstimmen aus der Testimonials Collection
 */
export const TestimonialsBlock: Block = {
  slug: 'testimonials-block',
  labels: {
    singular: 'Testimonials',
    plural: 'Testimonials',
  },
  imageURL: '/assets/blocks/testimonials.png',
  fields: [
    {
      name: 'title',
      type: 'text',
      defaultValue: 'Das sagen unsere Kunden',
      label: 'Überschrift',
    },
    {
      name: 'subtitle',
      type: 'text',
      label: 'Untertitel',
    },
    {
      name: 'layout',
      type: 'select',
      defaultValue: 'slider',
      label: 'Layout',
      options: [
        { label: 'Slider/Karussell', value: 'slider' },
        { label: 'Grid (Karten)', value: 'grid' },
        { label: 'Einzeln (Featured)', value: 'single' },
        { label: 'Masonry', value: 'masonry' },
        { label: 'Liste', value: 'list' },
      ],
    },
    {
      name: 'columns',
      type: 'select',
      defaultValue: '3',
      label: 'Spalten',
      options: [
        { label: '2 Spalten', value: '2' },
        { label: '3 Spalten', value: '3' },
        { label: '4 Spalten', value: '4' },
      ],
      admin: {
        condition: (data, siblingData) => 
          siblingData?.layout === 'grid' || 
          siblingData?.layout === 'masonry',
      },
    },
    {
      name: 'displayMode',
      type: 'select',
      defaultValue: 'all',
      label: 'Auswahl',
      options: [
        { label: 'Alle aktiven Testimonials', value: 'all' },
        { label: 'Handverlesene Auswahl', value: 'selected' },
      ],
    },
    {
      name: 'selectedTestimonials',
      type: 'relationship',
      relationTo: 'testimonials',
      hasMany: true,
      label: 'Testimonials auswählen',
      admin: {
        condition: (data, siblingData) => siblingData?.displayMode === 'selected',
      },
    },
    {
      name: 'limit',
      type: 'number',
      defaultValue: 6,
      min: 1,
      max: 20,
      label: 'Maximale Anzahl',
      admin: {
        condition: (data, siblingData) => siblingData?.displayMode === 'all',
      },
    },
    {
      name: 'showRating',
      type: 'checkbox',
      defaultValue: true,
      label: 'Sterne-Bewertung anzeigen',
    },
    {
      name: 'showImage',
      type: 'checkbox',
      defaultValue: true,
      label: 'Foto anzeigen',
    },
    {
      name: 'showCompany',
      type: 'checkbox',
      defaultValue: true,
      label: 'Unternehmen anzeigen',
    },
    {
      name: 'showSource',
      type: 'checkbox',
      defaultValue: false,
      label: 'Quelle anzeigen',
    },
    {
      name: 'autoplay',
      type: 'checkbox',
      defaultValue: true,
      label: 'Automatisch wechseln',
      admin: {
        condition: (data, siblingData) => siblingData?.layout === 'slider',
      },
    },
    {
      name: 'autoplaySpeed',
      type: 'number',
      defaultValue: 5000,
      min: 2000,
      max: 15000,
      label: 'Wechselintervall (ms)',
      admin: {
        condition: (data, siblingData) => 
          siblingData?.layout === 'slider' && siblingData?.autoplay,
      },
    },
    {
      name: 'backgroundColor',
      type: 'select',
      defaultValue: 'light',
      label: 'Hintergrund',
      options: [
        { label: 'Weiß', value: 'white' },
        { label: 'Hell (Grau)', value: 'light' },
        { label: 'Dunkel', value: 'dark' },
        { label: 'Akzentfarbe', value: 'accent' },
      ],
    },
  ],
}

/**
 * Newsletter Block
 * Anmeldeformular für Newsletter
 */
export const NewsletterBlock: Block = {
  slug: 'newsletter-block',
  labels: {
    singular: 'Newsletter Anmeldung',
    plural: 'Newsletter Anmeldungen',
  },
  imageURL: '/assets/blocks/newsletter.png',
  fields: [
    {
      name: 'title',
      type: 'text',
      defaultValue: 'Newsletter abonnieren',
      label: 'Überschrift',
    },
    {
      name: 'subtitle',
      type: 'textarea',
      defaultValue: 'Erhalten Sie regelmäßig Updates und Neuigkeiten direkt in Ihr Postfach.',
      label: 'Beschreibung',
    },
    {
      name: 'layout',
      type: 'select',
      defaultValue: 'inline',
      label: 'Layout',
      options: [
        { label: 'Inline (Eingabe + Button nebeneinander)', value: 'inline' },
        { label: 'Gestapelt (untereinander)', value: 'stacked' },
        { label: 'Mit Bild (50/50)', value: 'with-image' },
        { label: 'Minimal (nur Input)', value: 'minimal' },
        { label: 'Card (Karte)', value: 'card' },
      ],
    },
    {
      name: 'image',
      type: 'upload',
      relationTo: 'media',
      label: 'Bild',
      admin: {
        condition: (data, siblingData) => siblingData?.layout === 'with-image',
      },
    },
    {
      name: 'imagePosition',
      type: 'select',
      defaultValue: 'left',
      label: 'Bildposition',
      options: [
        { label: 'Links', value: 'left' },
        { label: 'Rechts', value: 'right' },
      ],
      admin: {
        condition: (data, siblingData) => siblingData?.layout === 'with-image',
      },
    },
    {
      name: 'collectName',
      type: 'checkbox',
      defaultValue: false,
      label: 'Name abfragen',
    },
    {
      name: 'showInterests',
      type: 'checkbox',
      defaultValue: false,
      label: 'Interessen zur Auswahl anbieten',
    },
    {
      name: 'availableInterests',
      type: 'select',
      hasMany: true,
      label: 'Verfügbare Interessen',
      options: [
        { label: 'Allgemeine Updates', value: 'general' },
        { label: 'Blog-Artikel', value: 'blog' },
        { label: 'Produkt-News', value: 'products' },
        { label: 'Angebote & Aktionen', value: 'offers' },
        { label: 'Events', value: 'events' },
      ],
      admin: {
        condition: (data, siblingData) => siblingData?.showInterests,
      },
    },
    {
      name: 'buttonText',
      type: 'text',
      defaultValue: 'Anmelden',
      label: 'Button-Text',
    },
    {
      name: 'placeholderEmail',
      type: 'text',
      defaultValue: 'Ihre E-Mail-Adresse',
      label: 'Placeholder E-Mail',
    },
    {
      name: 'successMessage',
      type: 'textarea',
      defaultValue: 'Vielen Dank! Bitte bestätigen Sie Ihre E-Mail-Adresse über den Link in der Bestätigungsmail.',
      label: 'Erfolgsmeldung',
    },
    {
      name: 'errorMessage',
      type: 'text',
      defaultValue: 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
      label: 'Fehlermeldung',
    },
    {
      name: 'privacyText',
      type: 'textarea',
      defaultValue: 'Mit der Anmeldung akzeptieren Sie unsere Datenschutzerklärung. Sie können sich jederzeit abmelden.',
      label: 'Datenschutz-Hinweis',
    },
    {
      name: 'privacyLink',
      type: 'text',
      defaultValue: '/datenschutz',
      label: 'Link zur Datenschutzerklärung',
    },
    {
      name: 'source',
      type: 'text',
      defaultValue: 'website',
      label: 'Tracking-Quelle',
      admin: {
        description: 'Wird gespeichert um zu tracken, wo die Anmeldung erfolgte',
      },
    },
    {
      name: 'backgroundColor',
      type: 'select',
      defaultValue: 'accent',
      label: 'Hintergrund',
      options: [
        { label: 'Weiß', value: 'white' },
        { label: 'Hell (Grau)', value: 'light' },
        { label: 'Dunkel', value: 'dark' },
        { label: 'Akzentfarbe', value: 'accent' },
      ],
    },
  ],
}

/**
 * Process Steps Block
 * Zeigt Prozess-Schritte / "So funktioniert es"
 */
export const ProcessStepsBlock: Block = {
  slug: 'process-steps-block',
  labels: {
    singular: 'Prozess/Schritte',
    plural: 'Prozess/Schritte',
  },
  imageURL: '/assets/blocks/process-steps.png',
  fields: [
    {
      name: 'title',
      type: 'text',
      defaultValue: 'So funktioniert es',
      label: 'Überschrift',
    },
    {
      name: 'subtitle',
      type: 'text',
      label: 'Untertitel',
    },
    {
      name: 'layout',
      type: 'select',
      defaultValue: 'horizontal',
      label: 'Layout',
      options: [
        { label: 'Horizontal (nebeneinander)', value: 'horizontal' },
        { label: 'Vertikal (untereinander)', value: 'vertical' },
        { label: 'Alternierend (Zickzack)', value: 'alternating' },
        { label: 'Mit Verbindungslinien', value: 'connected' },
        { label: 'Timeline-Stil', value: 'timeline' },
      ],
    },
    {
      name: 'showNumbers',
      type: 'checkbox',
      defaultValue: true,
      label: 'Schritt-Nummern anzeigen',
    },
    {
      name: 'showIcons',
      type: 'checkbox',
      defaultValue: true,
      label: 'Icons anzeigen',
    },
    {
      name: 'steps',
      type: 'array',
      label: 'Schritte',
      minRows: 2,
      maxRows: 10,
      fields: [
        {
          name: 'title',
          type: 'text',
          required: true,
          label: 'Schritt-Titel',
        },
        {
          name: 'description',
          type: 'textarea',
          label: 'Beschreibung',
        },
        {
          name: 'icon',
          type: 'text',
          label: 'Icon',
          admin: {
            description: 'Emoji oder Icon-Name (z.B. "📞", "✓", "1")',
          },
        },
        {
          name: 'image',
          type: 'upload',
          relationTo: 'media',
          label: 'Bild (optional)',
        },
      ],
    },
    {
      name: 'cta',
      type: 'group',
      label: 'Call-to-Action',
      fields: [
        {
          name: 'show',
          type: 'checkbox',
          defaultValue: false,
          label: 'CTA anzeigen',
        },
        {
          name: 'label',
          type: 'text',
          defaultValue: 'Jetzt starten',
          label: 'Button-Text',
          admin: {
            condition: (data, siblingData) => siblingData?.show,
          },
        },
        {
          name: 'href',
          type: 'text',
          label: 'Link',
          admin: {
            condition: (data, siblingData) => siblingData?.show,
          },
        },
        {
          name: 'variant',
          type: 'select',
          defaultValue: 'default',
          label: 'Button-Stil',
          options: [
            { label: 'Standard', value: 'default' },
            { label: 'Ghost', value: 'ghost' },
            { label: 'Light', value: 'light' },
          ],
          admin: {
            condition: (data, siblingData) => siblingData?.show,
          },
        },
      ],
    },
    {
      name: 'backgroundColor',
      type: 'select',
      defaultValue: 'white',
      label: 'Hintergrund',
      options: [
        { label: 'Weiß', value: 'white' },
        { label: 'Hell (Grau)', value: 'light' },
        { label: 'Dunkel', value: 'dark' },
      ],
    },
  ],
}

/**
 * Timeline Block (erweitert)
 * Chronologische Darstellung von Ereignissen
 */
export const TimelineBlock: Block = {
  slug: 'timeline-block',
  labels: {
    singular: 'Timeline',
    plural: 'Timelines',
  },
  imageURL: '/assets/blocks/timeline.png',
  fields: [
    {
      name: 'title',
      type: 'text',
      label: 'Überschrift',
    },
    {
      name: 'subtitle',
      type: 'text',
      label: 'Untertitel',
    },
    {
      name: 'layout',
      type: 'select',
      defaultValue: 'vertical',
      label: 'Layout',
      options: [
        { label: 'Vertikal (Standard)', value: 'vertical' },
        { label: 'Alternierend (links/rechts)', value: 'alternating' },
        { label: 'Horizontal (Zeitleiste)', value: 'horizontal' },
      ],
    },
    {
      name: 'showConnector',
      type: 'checkbox',
      defaultValue: true,
      label: 'Verbindungslinie anzeigen',
    },
    {
      name: 'markerStyle',
      type: 'select',
      defaultValue: 'dot',
      label: 'Marker-Stil',
      options: [
        { label: 'Punkt', value: 'dot' },
        { label: 'Nummer', value: 'number' },
        { label: 'Icon', value: 'icon' },
        { label: 'Jahr/Datum', value: 'date' },
      ],
    },
    {
      name: 'items',
      type: 'array',
      label: 'Einträge',
      minRows: 1,
      fields: [
        {
          name: 'year',
          type: 'text',
          label: 'Jahr/Datum',
          admin: {
            description: 'z.B. "2024", "Januar 2024", "15.03.2024"',
          },
        },
        {
          name: 'title',
          type: 'text',
          required: true,
          label: 'Titel',
        },
        {
          name: 'description',
          type: 'textarea',
          label: 'Beschreibung',
        },
        {
          name: 'icon',
          type: 'text',
          label: 'Icon',
          admin: {
            description: 'Emoji oder Icon-Name',
          },
        },
        {
          name: 'image',
          type: 'upload',
          relationTo: 'media',
          label: 'Bild (optional)',
        },
        {
          name: 'link',
          type: 'group',
          label: 'Link (optional)',
          fields: [
            {
              name: 'label',
              type: 'text',
              label: 'Link-Text',
            },
            {
              name: 'href',
              type: 'text',
              label: 'URL',
            },
          ],
        },
      ],
    },
    {
      name: 'backgroundColor',
      type: 'select',
      defaultValue: 'white',
      label: 'Hintergrund',
      options: [
        { label: 'Weiß', value: 'white' },
        { label: 'Hell (Grau)', value: 'light' },
        { label: 'Dunkel', value: 'dark' },
      ],
    },
  ],
}

// Export alle Blocks
export const universalBlocks = [
  PostsListBlock,
  TestimonialsBlock,
  NewsletterBlock,
  ProcessStepsBlock,
  TimelineBlock,
]

TEIL 3: Integration in Payload Config

3.1 payload.config.ts aktualisieren

// src/payload.config.ts

// === IMPORTS HINZUFÜGEN ===
import { Posts } from './collections/Posts'
import { Testimonials } from './collections/Testimonials'
import { NewsletterSubscribers } from './collections/NewsletterSubscribers'
import {
  PostsListBlock,
  TestimonialsBlock,
  NewsletterBlock,
  ProcessStepsBlock,
  TimelineBlock,
} from './blocks'

// === COLLECTIONS ARRAY ===
collections: [
  Users,
  Media,
  Tenants,
  Pages,
  Posts,                    // Erweitert
  Categories,
  Testimonials,             // NEU
  NewsletterSubscribers,    // NEU
  SocialLinks,
  CookieConfigurations,
  CookieInventory,
  ConsentLogs,
  PrivacyPolicySettings,
],

// === MULTI-TENANT PLUGIN ===
plugins: [
  multiTenantPlugin({
    tenantsSlug: 'tenants',
    collections: {
      'pages': {},
      'posts': {},
      'media': {},
      'categories': {},
      'testimonials': {},              // NEU
      'newsletter-subscribers': {},     // NEU
      'social-links': {},
      'cookie-configurations': {},
      'cookie-inventory': {},
      'privacy-policy-settings': {},
    },
  }),
],

3.2 Pages Collection: Blocks registrieren

In der Pages Collection (src/collections/Pages.ts) die neuen Blocks zum layout Feld hinzufügen:

// src/collections/Pages.ts

import {
  PostsListBlock,
  TestimonialsBlock,
  NewsletterBlock,
  ProcessStepsBlock,
  TimelineBlock,
} from '../blocks'

// Im fields Array:
{
  name: 'layout',
  type: 'blocks',
  label: 'Seiteninhalt',
  blocks: [
    // Bestehende Blocks...
    HeroBlock,
    TextBlock,
    ImageTextBlock,
    CardGridBlock,
    QuoteBlock,
    CTABlock,
    ContactFormBlock,
    DividerBlock,
    VideoBlock,
    
    // Neue Blocks
    PostsListBlock,
    TestimonialsBlock,
    NewsletterBlock,
    ProcessStepsBlock,
    TimelineBlock,
  ],
}

TEIL 4: Build und Migration

cd /home/payload/payload-cms

# TypeScript Types generieren
pnpm payload generate:types

# Migration erstellen (für neue Collections)
pnpm payload migrate:create

# Migration ausführen
pnpm payload migrate

# Build
pnpm build

# PM2 neu starten
pm2 restart payload

# Logs prüfen
pm2 logs payload --lines 50

TEIL 5: Verifizierung

API-Tests

# Posts mit neuem Type-Feld
curl -s "http://localhost:3000/api/posts?where[type][equals]=blog" | jq '.docs | length'

# Testimonials Collection
curl -s "http://localhost:3000/api/testimonials" | jq

# Newsletter Subscribers (sollte 403 ohne Auth)
curl -s "http://localhost:3000/api/newsletter-subscribers" | jq

# Newsletter Subscribe (POST - öffentlich)
curl -X POST "http://localhost:3000/api/newsletter-subscribers" \
  -H "Content-Type: application/json" \
  -d '{"email":"test@example.com","tenant":1}' | jq

Admin Panel prüfen

  1. Posts: Neues "Type" Dropdown in Sidebar
  2. Testimonials: Neue Collection unter "Content"
  3. Newsletter Subscribers: Neue Collection unter "Marketing"
  4. Pages Editor: Neue Blocks verfügbar

Zusammenfassung

Neue/Geänderte Dateien

Datei Aktion Beschreibung
src/collections/Posts.ts GEÄNDERT +type, +isFeatured, +excerpt
src/collections/Testimonials.ts NEU Kundenstimmen Collection
src/collections/NewsletterSubscribers.ts NEU Newsletter-Anmeldungen
src/blocks/index.ts NEU/ERWEITERT 5 Block-Definitionen
src/collections/Pages.ts GEÄNDERT Neue Blocks registriert
src/payload.config.ts GEÄNDERT Collections + Multi-Tenant

Collections

Collection Zweck Multi-Tenant
posts Blog, News, Presse
testimonials Kundenstimmen
newsletter-subscribers Anmeldungen

Blocks

Block Layouts Verwendung
posts-list-block grid, list, featured, compact, masonry Blog/News-Seiten
testimonials-block slider, grid, single, masonry, list Referenzen
newsletter-block inline, stacked, with-image, minimal, card Überall
process-steps-block horizontal, vertical, alternating, connected, timeline Service-Seiten
timeline-block vertical, alternating, horizontal Geschichte/Über uns

API Endpoints

Endpoint Methode Auth Beschreibung
/api/posts GET Public Blog/News abrufen
/api/posts?where[type][equals]=blog GET Public Nur Blog-Artikel
/api/testimonials GET Public Testimonials abrufen
/api/newsletter-subscribers GET Admin Subscribers lesen
/api/newsletter-subscribers POST Public Newsletter anmelden