Кастомные коллекции Payload CMS
Коллекция (Collection) в Payload — тип контента с REST API, GraphQL схемой и интерфейсом в admin panel, генерируемыми автоматически из TypeScript-конфигурации. Каждая коллекция хранится в отдельной таблице PostgreSQL или коллекции MongoDB.
Структура коллекции
// collections/Products.ts
import { CollectionConfig } from 'payload/types'
const Products: CollectionConfig = {
slug: 'products', // URL сегмент: /api/products
labels: {
singular: 'Товар',
plural: 'Товары',
},
admin: {
useAsTitle: 'name',
defaultColumns: ['name', 'price', 'category', 'inStock'],
group: 'Каталог',
},
// ...
}
Типы полей
fields: [
// Текстовые
{ name: 'name', type: 'text', required: true },
{ name: 'description', type: 'textarea' },
{ name: 'content', type: 'richText' },
// Числа и даты
{ name: 'price', type: 'number', min: 0, required: true },
{ name: 'publishedAt', type: 'date' },
// Выбор
{
name: 'status',
type: 'select',
options: [
{ label: 'Активен', value: 'active' },
{ label: 'Архив', value: 'archived' },
],
defaultValue: 'active',
},
// Медиа
{ name: 'image', type: 'upload', relationTo: 'media' },
// Связи
{
name: 'category',
type: 'relationship',
relationTo: 'categories',
hasMany: false,
},
{
name: 'tags',
type: 'relationship',
relationTo: 'tags',
hasMany: true,
},
// Массив объектов
{
name: 'variants',
type: 'array',
fields: [
{ name: 'sku', type: 'text', required: true },
{ name: 'color', type: 'text' },
{ name: 'size', type: 'text' },
{ name: 'stock', type: 'number', defaultValue: 0 },
],
},
// Блоки (как Gutenberg)
{
name: 'sections',
type: 'blocks',
blocks: [TextBlock, ImageBlock, CTABlock],
},
// Группа полей
{
name: 'seo',
type: 'group',
fields: [
{ name: 'title', type: 'text' },
{ name: 'description', type: 'textarea' },
],
},
]
Доступ (Access Control)
access: {
// Чтение — публичное
read: () => true,
// Создание — только авторизованные
create: ({ req: { user } }) => Boolean(user),
// Обновление — только владелец или admin
update: ({ req: { user }, id }) => {
if (!user) return false
if (user.role === 'admin') return true
return { author: { equals: user.id } } // фильтр-условие
},
// Удаление — только admin
delete: ({ req: { user } }) => user?.role === 'admin',
},
Хуки коллекции
hooks: {
beforeChange: [
async ({ data, req, operation }) => {
// Автоматический slug
if (operation === 'create' && !data.slug) {
data.slug = data.name
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^\w-]/g, '')
}
// Установить автора
if (operation === 'create' && req.user) {
data.author = req.user.id
}
return data
},
],
afterChange: [
async ({ doc, operation }) => {
// Инвалидация Next.js кэша
if (operation === 'update') {
await fetch(`/api/revalidate?path=/products/${doc.slug}`, {
method: 'POST',
})
}
},
],
afterDelete: [
async ({ doc }) => {
// Очистка связанных данных
console.log(`Product ${doc.id} deleted`)
},
],
},
Версионирование
versions: {
maxPerDoc: 20,
drafts: {
autosave: {
interval: 2000, // автосохранение каждые 2 секунды
},
},
},
Запросы к коллекции
// На сервере (Next.js Server Component)
import { getPayload } from 'payload'
import config from '@payload-config'
const payload = await getPayload({ config })
// Найти опубликованные товары в категории
const result = await payload.find({
collection: 'products',
where: {
and: [
{ status: { equals: 'active' } },
{ category: { equals: categoryId } },
{ price: { less_than: 10000 } },
],
},
sort: '-createdAt',
limit: 20,
page: 1,
depth: 2, // глубина populate связей
})
const { docs, totalDocs, hasNextPage } = result
REST API (автогенерируемый)
# Список
GET /api/products?where[status][equals]=active&limit=20
# Один документ
GET /api/products/:id
# Создать (требует авторизации)
POST /api/products
Authorization: Bearer <token>
Content-Type: application/json
# Обновить
PATCH /api/products/:id
# Удалить
DELETE /api/products/:id
Сроки
Настройка одной коллекции с полями, доступом и хуками — 2–4 часа. Полный каталог (5–10 взаимосвязанных коллекций) — 2–4 дня.







