mirror of
https://github.com/complexcaresolutions/cms.c2sgmbh.git
synced 2026-03-17 18:34:13 +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>
1437 lines
33 KiB
Markdown
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 |
|