mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 20:54:11 +00:00
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>
33 KiB
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
- Posts: Neues "Type" Dropdown in Sidebar
- Testimonials: Neue Collection unter "Content"
- Newsletter Subscribers: Neue Collection unter "Marketing"
- 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 |