Интеграция CMS Payload CMS для управления контентом
Payload — headless CMS на Node.js с открытым исходным кодом. Отличается от конкурентов тем, что сам является частью приложения: конфиг пишется на TypeScript и живёт в репозитории. Никаких внешних дашбордов, никакого vendor lock-in. Это не сервис — это библиотека, которую монтируешь в Express или Next.js.
Когда Payload имеет смысл
Продукт подходит, когда нужен полный контроль над схемой данных, кастомная аутентификация, или CMS нужно встроить в уже существующий backend. Payload не требует отдельного хостинга — он поднимается там же, где живёт API.
Не стоит использовать, если команда контент-менеджеров большая и привыкла к облачным CMS с гарантированным аптаймом — тогда Contentful или Prismic проще.
Структура проекта
my-project/
├── src/
│ ├── payload.config.ts # главный конфиг
│ ├── collections/ # типы контента
│ │ ├── Posts.ts
│ │ ├── Users.ts
│ │ └── Media.ts
│ ├── globals/ # singleton-документы
│ │ └── SiteSettings.ts
│ └── server.ts
Конфигурация коллекции
// src/collections/Posts.ts
import { CollectionConfig } from 'payload/types'
const Posts: CollectionConfig = {
slug: 'posts',
admin: {
useAsTitle: 'title',
defaultColumns: ['title', 'status', 'publishedAt'],
},
access: {
read: ({ req: { user } }) => {
if (user) return true
return { status: { equals: 'published' } }
},
create: ({ req: { user } }) => Boolean(user?.roles?.includes('editor')),
update: ({ req: { user } }) => Boolean(user?.roles?.includes('editor')),
},
versions: {
drafts: { autosave: true },
maxPerDoc: 20,
},
fields: [
{ name: 'title', type: 'text', required: true },
{ name: 'slug', type: 'text', unique: true, admin: { position: 'sidebar' } },
{
name: 'content',
type: 'richText',
editor: lexicalEditor({
features: ({ defaultFeatures }) => [
...defaultFeatures,
HTMLConverterFeature({}),
],
}),
},
{
name: 'featuredImage',
type: 'upload',
relationTo: 'media',
},
{
name: 'status',
type: 'select',
options: ['draft', 'published'],
defaultValue: 'draft',
admin: { position: 'sidebar' },
},
{
name: 'publishedAt',
type: 'date',
admin: { position: 'sidebar', date: { pickerAppearance: 'dayAndTime' } },
},
],
}
export default Posts
Главный конфиг
// src/payload.config.ts
import { buildConfig } from 'payload/config'
import { mongooseAdapter } from '@payloadcms/db-mongodb'
import { lexicalEditor } from '@payloadcms/richtext-lexical'
import Posts from './collections/Posts'
import Users from './collections/Users'
import Media from './collections/Media'
export default buildConfig({
serverURL: process.env.PAYLOAD_PUBLIC_SERVER_URL,
admin: {
user: Users.slug,
bundler: webpackBundler(),
},
editor: lexicalEditor({}),
collections: [Posts, Users, Media],
db: mongooseAdapter({ url: process.env.DATABASE_URI! }),
// либо PostgreSQL:
// db: postgresAdapter({ pool: { connectionString: process.env.DATABASE_URI } }),
upload: {
limits: { fileSize: 10_000_000 },
},
localization: {
locales: ['ru', 'en'],
defaultLocale: 'ru',
fallback: true,
},
})
Payload поддерживает MongoDB и PostgreSQL через официальные адаптеры. Для PostgreSQL миграции генерируются автоматически:
npx payload migrate:create
npx payload migrate
Интеграция с Next.js 14
Начиная с Payload 2.x поддерживается монтирование в Next.js App Router:
// app/(payload)/admin/[[...segments]]/page.tsx
import { RootPage } from '@payloadcms/next/views'
import config from '@payload-config'
export default RootPage.bind(null, { config })
// app/(payload)/api/[...slug]/route.ts
import { REST_DELETE, REST_GET, REST_PATCH, REST_POST } from '@payloadcms/next/routes'
import config from '@payload-config'
export const GET = REST_GET.bind(null, config)
export const POST = REST_POST.bind(null, config)
export const PATCH = REST_PATCH.bind(null, config)
export const DELETE = REST_DELETE.bind(null, config)
Это означает — один next start, один процесс, один деплой.
Запросы через Local API
Payload предоставляет Local API для серверного кода — без HTTP-оверхеда:
import payload from 'payload'
import config from '@payload-config'
await payload.init({ config })
const posts = await payload.find({
collection: 'posts',
where: {
status: { equals: 'published' },
publishedAt: { less_than_equal: new Date().toISOString() },
},
sort: '-publishedAt',
limit: 10,
depth: 2, // populate linked documents
})
REST API работает параллельно и доступен внешним клиентам:
GET /api/posts?where[status][equals]=published&sort=-publishedAt&limit=10
Хуки и расширения
Payload поддерживает хуки на уровне коллекций — до/после операций:
{
slug: 'posts',
hooks: {
beforeChange: [
async ({ data, operation }) => {
if (operation === 'create') {
data.slug = slugify(data.title)
}
return data
},
],
afterChange: [
async ({ doc }) => {
await revalidatePath(`/blog/${doc.slug}`)
},
],
},
}
Кастомные эндпоинты добавляются прямо в коллекцию:
endpoints: [
{
path: '/:id/publish',
method: 'post',
handler: async (req, res) => {
await payload.update({
collection: 'posts',
id: req.params.id,
data: { status: 'published', publishedAt: new Date() },
})
res.json({ message: 'Published' })
},
},
],
Медиа и загрузка файлов
const Media: CollectionConfig = {
slug: 'media',
upload: {
staticURL: '/media',
staticDir: 'media',
imageSizes: [
{ name: 'thumbnail', width: 400, height: 300, crop: 'centre' },
{ name: 'card', width: 768, height: 1024 },
{ name: 'hero', width: 1920, height: undefined },
],
adminThumbnail: 'thumbnail',
mimeTypes: ['image/*', 'application/pdf'],
},
fields: [{ name: 'alt', type: 'text' }],
}
Для S3 — официальный плагин @payloadcms/plugin-cloud-storage с адаптером под S3, GCS или Azure.
Сроки
Базовая установка с 3–4 коллекциями, локализацией и интеграцией в Next.js: 5–7 дней. Если нужна кастомная аутентификация, RBAC, сложные хуки — от 2 недель.







