import type { CollectionConfig } from 'payload' import { authenticatedOnly, tenantScopedPublicRead } from '../lib/tenantAccess' /** * Berechnet die geschätzte Lesezeit basierend auf Wortanzahl * Durchschnitt: ~200 Wörter pro Minute */ function calculateReadingTime(content: unknown): number { if (!content) return 1 // RichText zu Plain Text konvertieren (vereinfacht) let text = '' const extractText = (node: unknown): void => { if (!node || typeof node !== 'object') return const n = node as Record if (n.text && typeof n.text === 'string') { text += n.text + ' ' } if (Array.isArray(n.children)) { n.children.forEach(extractText) } if (n.root && typeof n.root === 'object') { extractText(n.root) } } extractText(content) const words = text.trim().split(/\s+/).filter(Boolean).length const minutes = Math.ceil(words / 200) return Math.max(1, minutes) } export const Posts: CollectionConfig = { slug: 'posts', admin: { useAsTitle: 'title', group: 'Content', defaultColumns: ['title', 'type', 'isFeatured', 'status', 'publishedAt'], }, access: { read: tenantScopedPublicRead, create: authenticatedOnly, update: authenticatedOnly, delete: authenticatedOnly, }, fields: [ { name: 'title', type: 'text', required: true, localized: true, }, { name: 'slug', type: 'text', required: true, localized: true, unique: false, // Uniqueness per locale handled by index admin: { description: 'URL-Pfad (z.B. "mein-beitrag" / "my-post")', }, }, // === 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, localized: true, 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, localized: true, }, { name: 'categories', type: 'relationship', relationTo: 'categories', hasMany: true, label: 'Kategorien', }, { name: 'tags', type: 'relationship', relationTo: 'tags', hasMany: true, label: 'Tags', admin: { description: 'Schlagwörter für bessere Auffindbarkeit', }, }, { name: 'author', type: 'relationship', relationTo: 'authors', label: 'Autor', admin: { description: 'Hauptautor des Beitrags', }, }, { name: 'coAuthors', type: 'relationship', relationTo: 'authors', hasMany: true, label: 'Co-Autoren', admin: { description: 'Weitere beteiligte Autoren', }, }, { name: 'authorLegacy', type: 'text', label: 'Autor (Legacy)', admin: { description: 'Freitext-Autor für ältere Beiträge ohne Autoren-Eintrag', condition: (_, siblingData) => !siblingData?.author, }, }, { name: 'readingTime', type: 'number', label: 'Lesezeit (Minuten)', admin: { position: 'sidebar', description: 'Wird automatisch berechnet', readOnly: true, }, }, { 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', localized: true, }, { name: 'metaDescription', type: 'textarea', label: 'Meta-Beschreibung', maxLength: 160, localized: true, }, { name: 'ogImage', type: 'upload', relationTo: 'media', label: 'Social Media Bild', }, ], }, ], hooks: { beforeChange: [ ({ data }) => { // Automatische Lesezeit-Berechnung if (data?.content) { data.readingTime = calculateReadingTime(data.content) } return data }, ], }, }