Skip to content

Instantly share code, notes, and snippets.

@koraysels
Created September 6, 2025 08:39
Show Gist options
  • Save koraysels/f13a677f7d98126b776bdb7e9462c51b to your computer and use it in GitHub Desktop.
Save koraysels/f13a677f7d98126b776bdb7e9462c51b to your computer and use it in GitHub Desktop.
content block lexical psyloadcms
import type { Block, Field } from 'payload'
import {
FixedToolbarFeature,
HeadingFeature,
InlineToolbarFeature,
lexicalEditor,
UploadFeature,
UnorderedListFeature,
OrderedListFeature,
} from '@payloadcms/richtext-lexical'
import { link } from '@/fields/link'
const columnFields: Field[] = [
{
name: 'size',
type: 'select',
defaultValue: 'oneThird',
options: [
{
label: 'One Third',
value: 'oneThird',
},
{
label: 'Half',
value: 'half',
},
{
label: 'Two Thirds',
value: 'twoThirds',
},
{
label: 'Full',
value: 'full',
},
],
},
{
name: 'richText',
type: 'richText',
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h2', 'h3', 'h4'] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
UnorderedListFeature(),
OrderedListFeature(),
UploadFeature({
collections: {
media: {
fields: [
{
name: 'caption',
type: 'richText',
editor: lexicalEditor(),
},
],
},
},
}),
]
},
}),
label: false,
},
{
name: 'enableLink',
type: 'checkbox',
},
link({
overrides: {
admin: {
condition: (_data, siblingData) => {
return Boolean(siblingData?.enableLink)
},
},
},
}),
]
export const Content: Block = {
slug: 'content',
interfaceName: 'ContentBlock',
fields: [
{
name: 'columns',
type: 'array',
admin: {
initCollapsed: true,
},
fields: columnFields,
},
],
}
import type { CollectionConfig } from 'payload'
import { authenticated } from '../../access/authenticated'
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
import { About } from '../../blocks/About/config'
import { Archive } from '../../blocks/ArchiveBlock/config'
import { Button } from '../../blocks/molecules/Button/config'
import { TitleConfig as Title } from '../../blocks/molecules/Title'
import { CallToAction } from '../../blocks/CallToAction/config'
import { Content } from '../../blocks/Content/config'
import { Divider } from '../../blocks/Divider/config'
import { FormBlock } from '../../blocks/Form/config'
import { GridPosts } from '../../blocks/GridPosts/config'
import { MediaBlock } from '../../blocks/MediaBlock/config'
import { hero } from '@/heros/config'
import { slugField } from '@/fields/slug'
import { populatePublishedAt } from '../../hooks/populatePublishedAt'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { revalidateDelete, revalidatePage } from './hooks/revalidatePage'
import { fixIdForLocalizedUpdates } from '../../hooks/fixIdForLocalizedUpdates'
import { cleanupDuplicateIds } from '../../hooks/cleanupDuplicateIds'
import {
MetaDescriptionField,
MetaImageField,
MetaTitleField,
OverviewField,
PreviewField,
} from '@payloadcms/plugin-seo/fields'
export const Pages: CollectionConfig<'pages'> = {
slug: 'pages',
access: {
create: authenticated,
delete: authenticated,
read: authenticatedOrPublished,
update: authenticated,
},
// This config controls what's populated by default when a page is referenced
// https://payloadcms.com/docs/queries/select#defaultpopulate-collection-config-property
// Type safe if the collection slug generic is passed to `CollectionConfig` - `CollectionConfig<'pages'>
defaultPopulate: {
title: true,
slug: true,
},
admin: {
defaultColumns: ['title', 'slug', 'updatedAt'],
livePreview: {
url: ({ data, req }) => {
const path = generatePreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '',
collection: 'pages',
req,
})
return path
},
},
preview: (data, { req }) =>
generatePreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '',
collection: 'pages',
req,
}),
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
localized: true,
},
{
type: 'tabs',
tabs: [
{
fields: [
{
name: 'layout',
type: 'blocks',
blocks: [Title, Button, CallToAction, Content, About, MediaBlock, Archive, FormBlock, GridPosts, Divider],
localized: true,
admin: {
initCollapsed: true,
},
},
],
label: 'Content',
},
{
fields: [hero],
label: 'Hero',
},
{
name: 'meta',
label: 'SEO',
localized: true,
fields: [
OverviewField({
titlePath: 'meta.title',
descriptionPath: 'meta.description',
imagePath: 'meta.image',
}),
MetaTitleField({
hasGenerateFn: true,
}),
MetaImageField({
relationTo: 'media',
}),
MetaDescriptionField({}),
PreviewField({
// if the `generateUrl` function is configured
hasGenerateFn: true,
// field paths to match the target field for data
titlePath: 'meta.title',
descriptionPath: 'meta.description',
}),
],
},
],
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
admin: {
position: 'sidebar',
},
label: 'Featured Image',
},
{
name: 'subtitle',
type: 'text',
localized: true,
admin: {
position: 'sidebar',
},
label: 'Subtitle',
},
{
name: 'shortDescription',
type: 'textarea',
localized: true,
admin: {
position: 'sidebar',
},
label: 'Short Description',
},
{
name: 'backgroundColor',
type: 'select',
options: [
{
label: 'Default',
value: 'default',
},
{
label: 'Purple',
value: 'purple',
},
{
label: 'Yellow',
value: 'yellow',
},
{
label: 'Green',
value: 'green',
},
{
label: 'Mint',
value: 'mint',
},
{
label: 'Blue',
value: 'blue',
},
{
label: 'Violet',
value: 'violet',
},
],
defaultValue: 'default',
admin: {
position: 'sidebar',
},
label: 'Background Color',
},
{
name: 'publishedAt',
type: 'date',
admin: {
position: 'sidebar',
},
},
...slugField(),
],
hooks: {
afterChange: [revalidatePage],
beforeChange: [fixIdForLocalizedUpdates, cleanupDuplicateIds, populatePublishedAt],
afterDelete: [revalidateDelete],
},
versions: {
drafts: {
autosave: false,
schedulePublish: true,
},
maxPerDoc: 50,
},
}
import type { CollectionConfig } from 'payload'
import {
BlocksFeature,
FixedToolbarFeature,
HeadingFeature,
HorizontalRuleFeature,
InlineToolbarFeature,
lexicalEditor,
} from '@payloadcms/richtext-lexical'
import { authenticated } from '../../access/authenticated'
import { authenticatedOrPublished } from '../../access/authenticatedOrPublished'
import { Banner } from '../../blocks/Banner/config'
import { Button } from '../../blocks/molecules/Button/config'
import { TitleConfig as Title } from '../../blocks/molecules/Title'
import { Code } from '../../blocks/Code/config'
import { Divider } from '../../blocks/Divider/config'
import { MediaBlock } from '../../blocks/MediaBlock/config'
import { generatePreviewPath } from '../../utilities/generatePreviewPath'
import { populateAuthors } from './hooks/populateAuthors'
import { revalidateDelete, revalidatePost } from './hooks/revalidatePost'
import { fixIdForLocalizedUpdates } from '../../hooks/fixIdForLocalizedUpdates'
import { cleanupDuplicateIds } from '../../hooks/cleanupDuplicateIds'
import {
MetaDescriptionField,
MetaImageField,
MetaTitleField,
OverviewField,
PreviewField,
} from '@payloadcms/plugin-seo/fields'
import { slugField } from '@/fields/slug'
export const Posts: CollectionConfig<'posts'> = {
slug: 'posts',
access: {
create: authenticated,
delete: authenticated,
read: authenticatedOrPublished,
update: authenticated,
},
// This config controls what's populated by default when a post is referenced
// https://payloadcms.com/docs/queries/select#defaultpopulate-collection-config-property
// Type safe if the collection slug generic is passed to `CollectionConfig` - `CollectionConfig<'posts'>
defaultPopulate: {
title: true,
slug: true,
categories: true,
image: true,
meta: {
image: true,
description: true,
},
},
admin: {
defaultColumns: ['title', 'slug', 'updatedAt'],
livePreview: {
url: ({ data, req }) => {
const path = generatePreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '',
collection: 'posts',
req,
})
return path
},
},
preview: (data, { req }) =>
generatePreviewPath({
slug: typeof data?.slug === 'string' ? data.slug : '',
collection: 'posts',
req,
}),
useAsTitle: 'title',
},
fields: [
{
name: 'title',
type: 'text',
required: true,
localized: true,
},
{
type: 'tabs',
tabs: [
{
fields: [
{
name: 'content',
type: 'richText',
localized: true,
editor: lexicalEditor({
features: ({ rootFeatures }) => {
return [
...rootFeatures,
HeadingFeature({ enabledHeadingSizes: ['h1', 'h2', 'h3', 'h4'] }),
BlocksFeature({ blocks: [Banner, Title, Button, Code, Divider, MediaBlock] }),
FixedToolbarFeature(),
InlineToolbarFeature(),
HorizontalRuleFeature(),
]
},
}),
label: false,
required: true,
},
],
label: 'Content',
},
{
fields: [
{
name: 'relatedPosts',
type: 'relationship',
admin: {
position: 'sidebar',
},
filterOptions: ({ id }) => {
return {
id: {
not_in: [id],
},
}
},
hasMany: true,
relationTo: 'posts',
},
{
name: 'categories',
type: 'relationship',
admin: {
position: 'sidebar',
},
hasMany: true,
relationTo: 'categories',
},
],
label: 'Meta',
},
{
name: 'meta',
label: 'SEO',
fields: [
OverviewField({
titlePath: 'meta.title',
descriptionPath: 'meta.description',
imagePath: 'meta.image',
}),
MetaTitleField({
hasGenerateFn: true,
}),
MetaImageField({
relationTo: 'media',
}),
MetaDescriptionField({}),
PreviewField({
// if the `generateUrl` function is configured
hasGenerateFn: true,
// field paths to match the target field for data
titlePath: 'meta.title',
descriptionPath: 'meta.description',
}),
],
},
],
},
{
name: 'image',
type: 'upload',
relationTo: 'media',
required: true,
admin: {
position: 'sidebar',
},
label: 'Featured Image',
},
{
name: 'publishedAt',
type: 'date',
admin: {
date: {
pickerAppearance: 'dayAndTime',
},
position: 'sidebar',
},
hooks: {
beforeChange: [
({ siblingData, value }) => {
if (siblingData._status === 'published' && !value) {
return new Date()
}
return value
},
],
},
},
{
name: 'authors',
type: 'relationship',
admin: {
position: 'sidebar',
},
hasMany: true,
relationTo: 'users',
},
// This field is only used to populate the user data via the `populateAuthors` hook
// This is because the `user` collection has access control locked to protect user privacy
// GraphQL will also not return mutated user data that differs from the underlying schema
{
name: 'populatedAuthors',
type: 'array',
access: {
update: () => false,
},
admin: {
disabled: true,
readOnly: true,
},
fields: [
{
name: 'id',
type: 'text',
},
{
name: 'name',
type: 'text',
},
],
},
...slugField(),
],
hooks: {
afterChange: [revalidatePost],
beforeChange: [fixIdForLocalizedUpdates, cleanupDuplicateIds],
afterRead: [populateAuthors],
afterDelete: [revalidateDelete],
},
versions: {
drafts: {
autosave: false,
schedulePublish: true,
},
maxPerDoc: 50,
},
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment