# 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: ```typescript // 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`: ```typescript // 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`: ```typescript // 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): ```typescript // 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 ```typescript // 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: ```typescript // 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 ```bash 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 ```bash # 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 |