feat: add content blocks and global settings

Blocks for page builder:
- HeroBlock: hero sections with CTA
- TextBlock: rich text content
- ImageTextBlock: image with text layout
- CardGridBlock: grid of cards
- CTABlock: call-to-action sections
- QuoteBlock: testimonial quotes
- VideoBlock: embedded videos
- DividerBlock: visual separators
- ContactFormBlock: contact forms
- NewsletterBlock: newsletter signup
- ProcessStepsBlock: step-by-step processes
- TimelineBlock: timeline displays
- TestimonialsBlock: testimonial carousels
- PostsListBlock: blog post listings

Globals:
- Navigation: site navigation structure
- SiteSettings: general site configuration
- SEOSettings: default SEO settings per tenant

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Martin Porwoll 2025-12-01 08:19:15 +00:00
parent 885ec93748
commit 95c9d2a4bc
18 changed files with 1820 additions and 0 deletions

67
src/blocks/CTABlock.ts Normal file
View file

@ -0,0 +1,67 @@
import type { Block } from 'payload'
export const CTABlock: Block = {
slug: 'cta-block',
labels: {
singular: 'Call-to-Action',
plural: 'Call-to-Actions',
},
fields: [
{
name: 'headline',
type: 'text',
required: true,
label: 'Überschrift',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
name: 'buttons',
type: 'array',
label: 'Buttons',
maxRows: 2,
fields: [
{
name: 'text',
type: 'text',
required: true,
label: 'Button-Text',
localized: true,
},
{
name: 'link',
type: 'text',
required: true,
label: 'Link',
},
{
name: 'style',
type: 'select',
defaultValue: 'primary',
label: 'Stil',
options: [
{ label: 'Primär', value: 'primary' },
{ label: 'Sekundär', value: 'secondary' },
{ label: 'Outline', value: 'outline' },
],
},
],
},
{
name: 'backgroundColor',
type: 'select',
defaultValue: 'dark',
label: 'Hintergrundfarbe',
options: [
{ label: 'Dunkel', value: 'dark' },
{ label: 'Hell', value: 'light' },
{ label: 'Akzent', value: 'accent' },
],
},
],
}

View file

@ -0,0 +1,68 @@
import type { Block } from 'payload'
export const CardGridBlock: Block = {
slug: 'card-grid-block',
labels: {
singular: 'Karten-Raster',
plural: 'Karten-Raster',
},
fields: [
{
name: 'headline',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'cards',
type: 'array',
label: 'Karten',
minRows: 1,
maxRows: 6,
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Bild',
},
{
name: 'title',
type: 'text',
required: true,
label: 'Titel',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
name: 'link',
type: 'text',
label: 'Link',
},
{
name: 'linkText',
type: 'text',
defaultValue: 'mehr',
label: 'Link-Text',
localized: true,
},
],
},
{
name: 'columns',
type: 'select',
defaultValue: '3',
label: 'Spalten',
options: [
{ label: '2 Spalten', value: '2' },
{ label: '3 Spalten', value: '3' },
{ label: '4 Spalten', value: '4' },
],
},
],
}

View file

@ -0,0 +1,48 @@
import type { Block } from 'payload'
export const ContactFormBlock: Block = {
slug: 'contact-form-block',
labels: {
singular: 'Kontaktformular',
plural: 'Kontaktformulare',
},
fields: [
{
name: 'headline',
type: 'text',
defaultValue: 'Kontakt',
label: 'Überschrift',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
name: 'recipientEmail',
type: 'email',
defaultValue: 'info@porwoll.de',
label: 'Empfänger E-Mail',
},
{
name: 'showPhone',
type: 'checkbox',
defaultValue: true,
label: 'Telefon anzeigen',
},
{
name: 'showAddress',
type: 'checkbox',
defaultValue: true,
label: 'Adresse anzeigen',
},
{
name: 'showSocials',
type: 'checkbox',
defaultValue: true,
label: 'Social Media anzeigen',
},
],
}

View file

@ -0,0 +1,33 @@
import type { Block } from 'payload'
export const DividerBlock: Block = {
slug: 'divider-block',
labels: {
singular: 'Trenner',
plural: 'Trenner',
},
fields: [
{
name: 'style',
type: 'select',
defaultValue: 'space',
label: 'Stil',
options: [
{ label: 'Linie', value: 'line' },
{ label: 'Abstand', value: 'space' },
{ label: 'Punkte', value: 'dots' },
],
},
{
name: 'spacing',
type: 'select',
defaultValue: 'medium',
label: 'Abstand',
options: [
{ label: 'Klein', value: 'small' },
{ label: 'Mittel', value: 'medium' },
{ label: 'Groß', value: 'large' },
],
},
],
}

76
src/blocks/HeroBlock.ts Normal file
View file

@ -0,0 +1,76 @@
import type { Block } from 'payload'
export const HeroBlock: Block = {
slug: 'hero-block',
labels: {
singular: 'Hero',
plural: 'Heroes',
},
fields: [
{
name: 'backgroundImage',
type: 'upload',
relationTo: 'media',
label: 'Hintergrundbild',
},
{
name: 'headline',
type: 'text',
required: true,
label: 'Überschrift',
localized: true,
},
{
name: 'subline',
type: 'textarea',
label: 'Unterüberschrift',
localized: true,
},
{
name: 'alignment',
type: 'select',
defaultValue: 'center',
label: 'Ausrichtung',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Zentriert', value: 'center' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'overlay',
type: 'checkbox',
defaultValue: true,
label: 'Dunkles Overlay',
},
{
name: 'cta',
type: 'group',
label: 'Call-to-Action',
fields: [
{
name: 'text',
type: 'text',
label: 'Button-Text',
localized: true,
},
{
name: 'link',
type: 'text',
label: 'Link',
},
{
name: 'style',
type: 'select',
defaultValue: 'primary',
label: 'Button-Stil',
options: [
{ label: 'Primär', value: 'primary' },
{ label: 'Sekundär', value: 'secondary' },
{ label: 'Outline', value: 'outline' },
],
},
],
},
],
}

View file

@ -0,0 +1,58 @@
import type { Block } from 'payload'
export const ImageTextBlock: Block = {
slug: 'image-text-block',
labels: {
singular: 'Bild & Text',
plural: 'Bild & Text',
},
fields: [
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
label: 'Bild',
},
{
name: 'imagePosition',
type: 'select',
defaultValue: 'left',
label: 'Bildposition',
options: [
{ label: 'Links', value: 'left' },
{ label: 'Rechts', value: 'right' },
],
},
{
name: 'headline',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'content',
type: 'richText',
label: 'Inhalt',
localized: true,
},
{
name: 'cta',
type: 'group',
label: 'Call-to-Action',
fields: [
{
name: 'text',
type: 'text',
label: 'Button-Text',
localized: true,
},
{
name: 'link',
type: 'text',
label: 'Link',
},
],
},
],
}

View file

@ -0,0 +1,156 @@
import type { Block } from 'payload'
/**
* Newsletter Block
* Anmeldeformular für Newsletter
*/
export const NewsletterBlock: Block = {
slug: 'newsletter-block',
labels: {
singular: 'Newsletter Anmeldung',
plural: 'Newsletter Anmeldungen',
},
fields: [
{
name: 'title',
type: 'text',
defaultValue: 'Newsletter abonnieren',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'textarea',
defaultValue: 'Erhalten Sie regelmäßig Updates und Neuigkeiten direkt in Ihr Postfach.',
label: 'Beschreibung',
localized: true,
},
{
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',
localized: true,
},
{
name: 'placeholderEmail',
type: 'text',
defaultValue: 'Ihre E-Mail-Adresse',
label: 'Placeholder E-Mail',
localized: true,
},
{
name: 'successMessage',
type: 'textarea',
defaultValue:
'Vielen Dank! Bitte bestätigen Sie Ihre E-Mail-Adresse über den Link in der Bestätigungsmail.',
label: 'Erfolgsmeldung',
localized: true,
},
{
name: 'errorMessage',
type: 'text',
defaultValue: 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es später erneut.',
label: 'Fehlermeldung',
localized: true,
},
{
name: 'privacyText',
type: 'textarea',
defaultValue:
'Mit der Anmeldung akzeptieren Sie unsere Datenschutzerklärung. Sie können sich jederzeit abmelden.',
label: 'Datenschutz-Hinweis',
localized: true,
},
{
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' },
],
},
],
}

View file

@ -0,0 +1,159 @@
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',
},
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
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',
localized: true,
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' },
],
},
],
}

View file

@ -0,0 +1,145 @@
import type { Block } from 'payload'
/**
* Process Steps Block
* Zeigt Prozess-Schritte / "So funktioniert es"
*/
export const ProcessStepsBlock: Block = {
slug: 'process-steps-block',
labels: {
singular: 'Prozess/Schritte',
plural: 'Prozess/Schritte',
},
fields: [
{
name: 'title',
type: 'text',
defaultValue: 'So funktioniert es',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
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',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
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',
localized: true,
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' },
],
},
],
}

47
src/blocks/QuoteBlock.ts Normal file
View file

@ -0,0 +1,47 @@
import type { Block } from 'payload'
export const QuoteBlock: Block = {
slug: 'quote-block',
labels: {
singular: 'Zitat',
plural: 'Zitate',
},
fields: [
{
name: 'quote',
type: 'textarea',
required: true,
label: 'Zitat',
localized: true,
},
{
name: 'author',
type: 'text',
label: 'Autor',
// Author bleibt nicht lokalisiert - Namen sind sprachunabhängig
},
{
name: 'role',
type: 'text',
label: 'Rolle/Position',
localized: true,
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
label: 'Autorenbild',
},
{
name: 'style',
type: 'select',
defaultValue: 'simple',
label: 'Stil',
options: [
{ label: 'Einfach', value: 'simple' },
{ label: 'Hervorgehoben', value: 'highlighted' },
{ label: 'Mit Bild', value: 'with-image' },
],
},
],
}

View file

@ -0,0 +1,145 @@
import type { Block } from 'payload'
/**
* Testimonials Block
* Zeigt Kundenstimmen aus der Testimonials Collection
*/
export const TestimonialsBlock: Block = {
slug: 'testimonials-block',
labels: {
singular: 'Testimonials',
plural: 'Testimonials',
},
fields: [
{
name: 'title',
type: 'text',
defaultValue: 'Das sagen unsere Kunden',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
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',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
relationTo: 'testimonials' as any,
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' },
],
},
],
}

29
src/blocks/TextBlock.ts Normal file
View file

@ -0,0 +1,29 @@
import type { Block } from 'payload'
export const TextBlock: Block = {
slug: 'text-block',
labels: {
singular: 'Text',
plural: 'Texte',
},
fields: [
{
name: 'content',
type: 'richText',
required: true,
label: 'Inhalt',
localized: true,
},
{
name: 'width',
type: 'select',
defaultValue: 'medium',
label: 'Breite',
options: [
{ label: 'Schmal', value: 'narrow' },
{ label: 'Mittel', value: 'medium' },
{ label: 'Volle Breite', value: 'full' },
],
},
],
}

128
src/blocks/TimelineBlock.ts Normal file
View file

@ -0,0 +1,128 @@
import type { Block } from 'payload'
/**
* Timeline Block (erweitert)
* Chronologische Darstellung von Ereignissen
*/
export const TimelineBlock: Block = {
slug: 'timeline-block',
labels: {
singular: 'Timeline',
plural: 'Timelines',
},
fields: [
{
name: 'title',
type: 'text',
label: 'Überschrift',
localized: true,
},
{
name: 'subtitle',
type: 'text',
label: 'Untertitel',
localized: true,
},
{
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',
localized: true,
},
{
name: 'description',
type: 'textarea',
label: 'Beschreibung',
localized: true,
},
{
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',
localized: true,
},
{
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' },
],
},
],
}

37
src/blocks/VideoBlock.ts Normal file
View file

@ -0,0 +1,37 @@
import type { Block } from 'payload'
export const VideoBlock: Block = {
slug: 'video-block',
labels: {
singular: 'Video',
plural: 'Videos',
},
fields: [
{
name: 'videoUrl',
type: 'text',
required: true,
label: 'Video-URL',
admin: {
description: 'YouTube oder Vimeo URL',
},
},
{
name: 'caption',
type: 'text',
label: 'Beschriftung',
localized: true,
},
{
name: 'aspectRatio',
type: 'select',
defaultValue: '16:9',
label: 'Seitenverhältnis',
options: [
{ label: '16:9', value: '16:9' },
{ label: '4:3', value: '4:3' },
{ label: '1:1', value: '1:1' },
],
},
],
}

16
src/blocks/index.ts Normal file
View file

@ -0,0 +1,16 @@
export { HeroBlock } from './HeroBlock'
export { TextBlock } from './TextBlock'
export { ImageTextBlock } from './ImageTextBlock'
export { CardGridBlock } from './CardGridBlock'
export { QuoteBlock } from './QuoteBlock'
export { CTABlock } from './CTABlock'
export { ContactFormBlock } from './ContactFormBlock'
export { TimelineBlock } from './TimelineBlock'
export { DividerBlock } from './DividerBlock'
export { VideoBlock } from './VideoBlock'
// Neue universelle Blocks
export { PostsListBlock } from './PostsListBlock'
export { TestimonialsBlock } from './TestimonialsBlock'
export { NewsletterBlock } from './NewsletterBlock'
export { ProcessStepsBlock } from './ProcessStepsBlock'

131
src/globals/Navigation.ts Normal file
View file

@ -0,0 +1,131 @@
import type { GlobalConfig } from 'payload'
export const Navigation: GlobalConfig = {
slug: 'navigation',
label: 'Navigation',
access: {
read: () => true,
update: ({ req }) => !!req.user,
},
fields: [
{
name: 'mainMenu',
type: 'array',
label: 'Hauptmenü',
fields: [
{
name: 'label',
type: 'text',
required: true,
localized: true,
},
{
name: 'type',
type: 'select',
defaultValue: 'page',
options: [
{ label: 'Seite', value: 'page' },
{ label: 'Eigener Link', value: 'custom' },
{ label: 'Untermenü', value: 'submenu' },
],
},
{
name: 'page',
type: 'relationship',
relationTo: 'pages',
admin: {
condition: (data, siblingData) => siblingData?.type === 'page',
},
},
{
name: 'url',
type: 'text',
admin: {
condition: (data, siblingData) => siblingData?.type === 'custom',
},
},
{
name: 'openInNewTab',
type: 'checkbox',
defaultValue: false,
},
{
name: 'submenu',
type: 'array',
admin: {
condition: (data, siblingData) => siblingData?.type === 'submenu',
},
fields: [
{
name: 'label',
type: 'text',
required: true,
localized: true,
},
{
name: 'linkType',
type: 'select',
defaultValue: 'page',
options: [
{ label: 'Seite', value: 'page' },
{ label: 'Eigener Link', value: 'custom' },
],
},
{
name: 'page',
type: 'relationship',
relationTo: 'pages',
admin: {
condition: (data, siblingData) => siblingData?.linkType === 'page',
},
},
{
name: 'url',
type: 'text',
admin: {
condition: (data, siblingData) => siblingData?.linkType === 'custom',
},
},
],
},
],
},
{
name: 'footerMenu',
type: 'array',
label: 'Footer-Menü',
fields: [
{
name: 'label',
type: 'text',
required: true,
localized: true,
},
{
name: 'linkType',
type: 'select',
defaultValue: 'page',
options: [
{ label: 'Seite', value: 'page' },
{ label: 'Eigener Link', value: 'custom' },
],
},
{
name: 'page',
type: 'relationship',
relationTo: 'pages',
admin: {
condition: (data, siblingData) => siblingData?.linkType === 'page',
},
},
{
name: 'url',
type: 'text',
admin: {
condition: (data, siblingData) => siblingData?.linkType === 'custom',
},
},
],
},
],
}

385
src/globals/SEOSettings.ts Normal file
View file

@ -0,0 +1,385 @@
import type { GlobalConfig } from 'payload'
/**
* SEO Settings Global
*
* Globale SEO-Einstellungen pro Tenant für:
* - Meta-Defaults
* - Social Media Profile
* - Schema.org Organisation
* - robots.txt Regeln
*/
export const SEOSettings: GlobalConfig = {
slug: 'seo-settings',
label: 'SEO Einstellungen',
admin: {
group: 'Einstellungen',
description: 'Globale SEO-Konfiguration und Schema.org Daten',
},
fields: [
// === META DEFAULTS ===
{
name: 'metaDefaults',
type: 'group',
label: 'Standard Meta-Tags',
fields: [
{
name: 'titleSuffix',
type: 'text',
label: 'Titel-Suffix',
defaultValue: '| Website',
localized: true,
admin: {
description: 'Wird an jeden Seitentitel angehängt (z.B. "Startseite | Firmenname")',
},
},
{
name: 'defaultDescription',
type: 'textarea',
label: 'Standard Meta-Beschreibung',
maxLength: 160,
localized: true,
admin: {
description: 'Wird verwendet, wenn keine spezifische Beschreibung gesetzt ist',
},
},
{
name: 'defaultOgImage',
type: 'upload',
relationTo: 'media',
label: 'Standard Social Media Bild',
admin: {
description: 'Fallback-Bild für Social Media Shares (empfohlen: 1200x630px)',
},
},
{
name: 'keywords',
type: 'text',
hasMany: true,
label: 'Standard Keywords',
admin: {
description: 'Globale Keywords (optional, geringe SEO-Relevanz)',
},
},
],
},
// === ORGANIZATION SCHEMA ===
{
name: 'organization',
type: 'group',
label: 'Organisation (Schema.org)',
admin: {
description: 'Daten für das Organization Schema',
},
fields: [
{
name: 'name',
type: 'text',
label: 'Firmenname',
required: true,
},
{
name: 'legalName',
type: 'text',
label: 'Rechtlicher Name',
admin: {
description: 'Vollständiger rechtlicher Firmenname (falls abweichend)',
},
},
{
name: 'description',
type: 'textarea',
label: 'Unternehmensbeschreibung',
maxLength: 300,
localized: true,
},
{
name: 'logo',
type: 'upload',
relationTo: 'media',
label: 'Logo',
admin: {
description: 'Firmenlogo für Schema.org (min. 112x112px, empfohlen: 512x512px)',
},
},
{
name: 'foundingDate',
type: 'date',
label: 'Gründungsdatum',
admin: {
date: {
pickerAppearance: 'dayOnly',
},
},
},
],
},
// === CONTACT INFO ===
{
name: 'contact',
type: 'group',
label: 'Kontaktdaten',
fields: [
{
name: 'email',
type: 'email',
label: 'E-Mail',
},
{
name: 'phone',
type: 'text',
label: 'Telefon',
admin: {
description: 'Im Format +49 123 456789',
},
},
{
name: 'fax',
type: 'text',
label: 'Fax',
},
],
},
// === ADDRESS ===
{
name: 'address',
type: 'group',
label: 'Adresse',
fields: [
{
name: 'street',
type: 'text',
label: 'Straße & Hausnummer',
},
{
name: 'postalCode',
type: 'text',
label: 'Postleitzahl',
},
{
name: 'city',
type: 'text',
label: 'Stadt',
},
{
name: 'region',
type: 'text',
label: 'Bundesland/Region',
},
{
name: 'country',
type: 'text',
label: 'Land',
defaultValue: 'Deutschland',
},
{
name: 'countryCode',
type: 'text',
label: 'Ländercode',
defaultValue: 'DE',
admin: {
description: 'ISO 3166-1 Alpha-2 Code',
},
},
],
},
// === GEO LOCATION ===
{
name: 'geo',
type: 'group',
label: 'Geo-Koordinaten',
admin: {
description: 'Für Local Business Schema',
},
fields: [
{
name: 'latitude',
type: 'number',
label: 'Breitengrad',
admin: {
step: 0.000001,
},
},
{
name: 'longitude',
type: 'number',
label: 'Längengrad',
admin: {
step: 0.000001,
},
},
],
},
// === SOCIAL PROFILES ===
{
name: 'socialProfiles',
type: 'array',
label: 'Social Media Profile',
admin: {
description: 'URLs zu Social Media Profilen (für sameAs Schema)',
},
fields: [
{
name: 'platform',
type: 'select',
label: 'Plattform',
options: [
{ label: 'Facebook', value: 'facebook' },
{ label: 'Instagram', value: 'instagram' },
{ label: 'Twitter/X', value: 'twitter' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'YouTube', value: 'youtube' },
{ label: 'TikTok', value: 'tiktok' },
{ label: 'Pinterest', value: 'pinterest' },
{ label: 'XING', value: 'xing' },
{ label: 'Andere', value: 'other' },
],
},
{
name: 'url',
type: 'text',
label: 'Profil-URL',
required: true,
},
],
},
// === LOCAL BUSINESS ===
{
name: 'localBusiness',
type: 'group',
label: 'Local Business',
admin: {
description: 'Zusätzliche Daten für lokale Unternehmen',
},
fields: [
{
name: 'enabled',
type: 'checkbox',
label: 'Local Business Schema aktivieren',
defaultValue: false,
},
{
name: 'type',
type: 'select',
label: 'Geschäftstyp',
options: [
{ label: 'Lokales Unternehmen', value: 'LocalBusiness' },
{ label: 'Arztpraxis', value: 'Physician' },
{ label: 'Zahnarzt', value: 'Dentist' },
{ label: 'Anwaltskanzlei', value: 'Attorney' },
{ label: 'Restaurant', value: 'Restaurant' },
{ label: 'Hotel', value: 'Hotel' },
{ label: 'Einzelhandel', value: 'Store' },
{ label: 'Fitnessstudio', value: 'HealthClub' },
{ label: 'Friseursalon', value: 'HairSalon' },
{ label: 'Autowerkstatt', value: 'AutoRepair' },
{ label: 'Immobilienmakler', value: 'RealEstateAgent' },
{ label: 'Finanzdienstleister', value: 'FinancialService' },
{ label: 'IT-Dienstleister', value: 'ProfessionalService' },
{ label: 'Pflegedienst', value: 'MedicalBusiness' },
],
admin: {
condition: (data) => data?.localBusiness?.enabled,
},
},
{
name: 'priceRange',
type: 'select',
label: 'Preiskategorie',
options: [
{ label: '€ (Günstig)', value: '€' },
{ label: '€€ (Mittel)', value: '€€' },
{ label: '€€€ (Gehoben)', value: '€€€' },
{ label: '€€€€ (Premium)', value: '€€€€' },
],
admin: {
condition: (data) => data?.localBusiness?.enabled,
},
},
{
name: 'openingHours',
type: 'array',
label: 'Öffnungszeiten',
admin: {
condition: (data) => data?.localBusiness?.enabled,
description: 'Im Format "Mo-Fr 09:00-17:00"',
},
fields: [
{
name: 'specification',
type: 'text',
label: 'Öffnungszeit',
admin: {
placeholder: 'Mo-Fr 09:00-17:00',
},
},
],
},
],
},
// === ROBOTS SETTINGS ===
{
name: 'robots',
type: 'group',
label: 'Robots & Indexierung',
fields: [
{
name: 'allowIndexing',
type: 'checkbox',
label: 'Indexierung erlauben',
defaultValue: true,
admin: {
description: 'Wenn deaktiviert, wird die gesamte Website von Suchmaschinen ausgeschlossen',
},
},
{
name: 'additionalDisallow',
type: 'text',
hasMany: true,
label: 'Zusätzliche Pfade ausschließen',
admin: {
description: 'Pfade die nicht gecrawlt werden sollen (z.B. "/intern", "/preview")',
condition: (data) => data?.robots?.allowIndexing,
},
},
],
},
// === VERIFICATION CODES ===
{
name: 'verification',
type: 'group',
label: 'Verifizierungscodes',
admin: {
description: 'Codes für Suchmaschinen-Verifizierung',
},
fields: [
{
name: 'google',
type: 'text',
label: 'Google Search Console',
admin: {
description: 'Meta-Tag Content (nur der Code, nicht das gesamte Tag)',
},
},
{
name: 'bing',
type: 'text',
label: 'Bing Webmaster Tools',
},
{
name: 'yandex',
type: 'text',
label: 'Yandex Webmaster',
},
],
},
],
}

View file

@ -0,0 +1,92 @@
import type { GlobalConfig } from 'payload'
export const SiteSettings: GlobalConfig = {
slug: 'site-settings',
label: 'Site Settings',
access: {
read: () => true,
update: ({ req }) => !!req.user,
},
fields: [
{
name: 'siteName',
type: 'text',
defaultValue: 'porwoll.de',
localized: true,
},
{
name: 'siteTagline',
type: 'text',
localized: true,
},
{
name: 'logo',
type: 'upload',
relationTo: 'media',
},
{
name: 'favicon',
type: 'upload',
relationTo: 'media',
},
{
name: 'contact',
type: 'group',
fields: [
{
name: 'email',
type: 'email',
},
{
name: 'phone',
type: 'text',
},
{
name: 'address',
type: 'textarea',
},
],
},
{
name: 'footer',
type: 'group',
fields: [
{
name: 'copyrightText',
type: 'text',
localized: true,
},
{
name: 'showSocialLinks',
type: 'checkbox',
defaultValue: true,
},
],
},
{
name: 'seo',
type: 'group',
label: 'SEO',
fields: [
{
name: 'defaultMetaTitle',
type: 'text',
label: 'Standard Meta-Titel',
localized: true,
},
{
name: 'defaultMetaDescription',
type: 'textarea',
label: 'Standard Meta-Beschreibung',
localized: true,
},
{
name: 'defaultOgImage',
type: 'upload',
relationTo: 'media',
label: 'Standard Social Media Bild',
},
],
},
],
}