Axil_Accountants/src/payload/collections/Posts.ts
Vadym Samoilenko 3f6dfe36b1 feat: full CMS integration — connect all content to Payload admin panel
- Connect homepage, blog, services, header, footer to Payload CMS via local API
- Add HomePage global with 7 editorial sections (hero, painPoints, solution, whyAxil, audience, process, finalCta)
- Add ServerHeader/ServerFooter async wrappers with unstable_cache + tag-based ISR revalidation
- Rewrite blog/[slug] and services/[slug] pages to fetch from CMS with fallbacks
- Add seed script (src/lib/seed.ts) to populate all collections and globals from hardcoded defaults
- Restructure app into (site)/ route group to fix Payload admin hydration conflicts
- Make root layout a passthrough; (site)/layout.tsx owns html/body/fonts
- Restrict user creation/update/delete to admin role only
- Fix migration imports: type MigrateUpArgs/Down from @payloadcms/db-postgres
- Wrap revalidateTag dynamic imports in try/catch for seed/CLI compatibility
- Skip migrations in dev mode (Payload handles schema push automatically)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-23 21:19:44 +00:00

129 lines
2.9 KiB
TypeScript

import type {
CollectionConfig,
CollectionAfterChangeHook,
CollectionAfterDeleteHook,
} from 'payload';
const calculateReadingTime: CollectionAfterChangeHook = async ({ doc, req: { payload } }) => {
if (!doc.content) return doc;
try {
// Rough estimate: 200 words per minute
const wordCount = JSON.stringify(doc.content).split(' ').length;
const minutes = Math.ceil(wordCount / 200);
await payload.update({ collection: 'posts', id: doc.id, data: { readingTime: minutes } });
} catch {
/* skip if update fails during seed */
}
return doc;
};
const revalidatePosts: CollectionAfterChangeHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('posts');
if (doc?.slug) revalidateTag(`post-${doc.slug}`);
} catch {
/* not in Next.js runtime */
}
return doc;
};
const revalidatePostsOnDelete: CollectionAfterDeleteHook = async ({ doc }) => {
try {
const { revalidateTag } = await import('next/cache');
revalidateTag('posts');
if (doc?.slug) revalidateTag(`post-${doc.slug}`);
} catch {
/* not in Next.js runtime */
}
};
export const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'status', 'publishedAt', 'author'],
},
access: {
read: () => true,
},
hooks: {
afterChange: [calculateReadingTime, revalidatePosts],
afterDelete: [revalidatePostsOnDelete],
},
fields: [
{
name: 'title',
type: 'text',
required: true,
},
{
name: 'slug',
type: 'text',
required: true,
unique: true,
},
{
name: 'author',
type: 'relationship',
relationTo: 'users',
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
},
{
name: 'coverImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'excerpt',
type: 'textarea',
admin: {
description: 'Short summary shown on listing pages (max 150 chars)',
},
},
{
name: 'content',
type: 'richText',
},
{
name: 'readingTime',
type: 'number',
admin: {
description: 'Auto-calculated on save (minutes)',
readOnly: true,
},
},
{
name: 'publishedAt',
type: 'date',
},
{
name: 'status',
type: 'select',
defaultValue: 'draft',
options: [
{ label: 'Draft', value: 'draft' },
{ label: 'Published', value: 'published' },
{ label: 'Scheduled', value: 'scheduled' },
],
},
{
name: 'tags',
type: 'array',
fields: [{ name: 'tag', type: 'text' }],
},
{
name: 'seo',
type: 'group',
fields: [
{ name: 'metaTitle', type: 'text' },
{ name: 'metaDescription', type: 'textarea' },
{ name: 'ogImage', type: 'upload', relationTo: 'media' },
],
},
],
};