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

1437 lines
33 KiB
Markdown

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