Разработка плагинов для Payload CMS
Представьте: у вас 10 коллекций, в каждую нужно добавить одинаковые SEO-поля — мета-заголовок, описание, изображение и флаг noIndex. Ручное добавление занимает 2-3 часа, но при изменении структуры поля придется править каждую коллекцию — это 20-30 часов на поддержку в год. Плагины Payload CMS решают эту проблему раз и навсегда: вы описываете поля один раз в коде плагина, а затем подключаете его к нужным коллекциям одной строкой. Мы разрабатываем такие плагины под ключ с гарантией качества и поддержкой. Наш опыт — более 5 лет и 15+ плагинов для разных проектов. Если вам нужен плагин, свяжитесь с нами для консультации.
Почему плагины выгодны?
Основная проблема — дублирование кода. Если функциональность нужна в нескольких коллекциях, копирование полей и хуков раздувает базу кода. Вторая проблема — сложность поддержки: изменение логики требует правки в каждом месте. Третья — отсутствие единого интерфейса для схожих операций. Плагины централизуют функциональность: SEO, аудит, поиск, кастомные эндпоинты. Например, плагин SEO добавляет одинаковые мета-поля во все указанные коллекции, а плагин аудита протоколирует все изменения в единую коллекцию логов. Сравнение с ручным добавлением: плагин в 3-4 раза быстрее в поддержке и менее подвержен ошибкам. Внедрение сокращает время на 30-40%.
Как разработать плагин для Payload CMS?
Согласно официальной документации Payload CMS, плагин — это "функция, которая принимает конфигурацию и возвращает измененную конфигурацию". Это не магия: плагин просто добавляет коллекции, поля, хуки, эндпоинты и компоненты к существующей конфигурации перед инициализацией CMS. Официальные плагины (@payloadcms/seo, @payloadcms/form-builder) следуют этой же модели.
// Тип плагина
type Plugin = (incomingConfig: Config) => Config
// Простейший плагин
const myPlugin: Plugin = (config) => {
return {
...config,
collections: [
...(config.collections || []),
// добавить коллекцию
],
hooks: {
...config.hooks,
afterInit: [
...(config.hooks?.afterInit || []),
// добавить хук
],
},
}
}
export default buildConfig({
plugins: [myPlugin],
})
Примеры плагинов: SEO, аудит, поиск
Плагин SEO добавляет мета-поля во все указанные коллекции:
// plugins/seo/index.ts
import type { Config, CollectionConfig, GlobalConfig } from 'payload/types'
interface SEOPluginConfig {
collections?: string[] // slug коллекций, куда добавить SEO-поля
globals?: string[]
uploadsCollection?: string
generateTitle?: (doc: any) => string
generateDescription?: (doc: any) => string
}
export const seoPlugin = (pluginConfig: SEOPluginConfig) => (config: Config): Config => {
const seoFields = [
{
name: 'meta',
type: 'group' as const,
label: 'SEO',
admin: { position: 'sidebar' as const },
fields: [
{
name: 'title',
type: 'text' as const,
admin: {
description: ({ doc }: any) =>
pluginConfig.generateTitle?.(doc) || 'Автозаполнение: заголовок документа',
},
},
{
name: 'description',
type: 'textarea' as const,
maxLength: 160,
},
{
name: 'image',
type: 'upload' as const,
relationTo: pluginConfig.uploadsCollection || 'media',
},
{
name: 'noIndex',
type: 'checkbox' as const,
defaultValue: false,
},
],
},
]
return {
...config,
collections: config.collections?.map(collection => {
if (pluginConfig.collections?.includes(collection.slug)) {
return {
...collection,
fields: [...(collection.fields || []), ...seoFields],
}
}
return collection
}),
globals: config.globals?.map(global => {
if (pluginConfig.globals?.includes(global.slug)) {
return {
...global,
fields: [...(global.fields || []), ...seoFields],
}
}
return global
}),
hooks: {
...config.hooks,
afterRead: [
...(config.hooks?.afterRead || []),
({ doc }: any) => {
if (!doc.meta?.title && pluginConfig.generateTitle) {
doc.meta = {
...doc.meta,
title: pluginConfig.generateTitle(doc),
}
}
return doc
},
],
},
}
}
Плагин аудита действий логирует все изменения:
// plugins/audit-log/index.ts
import type { Config } from 'payload/types'
interface AuditLogConfig {
collections: string[]
}
export const auditLogPlugin = ({ collections }: AuditLogConfig) => (config: Config): Config => {
const auditCollection = {
slug: 'audit-logs',
admin: { hidden: true },
access: {
read: ({ req }: any) => req.user?.role === 'admin',
create: () => false,
update: () => false,
delete: () => false,
},
fields: [
{ name: 'collection', type: 'text' as const },
{ name: 'docId', type: 'text' as const },
{ name: 'operation', type: 'text' as const },
{ name: 'user', type: 'relationship' as const, relationTo: 'users' as const },
{ name: 'before', type: 'json' as const },
{ name: 'after', type: 'json' as const },
{ name: 'timestamp', type: 'date' as const },
],
}
const auditedCollections = config.collections?.map(collection => {
if (!collections.includes(collection.slug)) return collection
return {
...collection,
hooks: {
...collection.hooks,
afterChange: [
...(collection.hooks?.afterChange || []),
async ({ doc, previousDoc, operation, req }: any) => {
if (!req.payload) return
await req.payload.create({
collection: 'audit-logs',
data: {
collection: collection.slug,
docId: String(doc.id),
operation,
user: req.user?.id,
before: previousDoc || null,
after: doc,
timestamp: new Date().toISOString(),
},
disableVerificationEmail: true,
})
},
],
},
}
})
return {
...config,
collections: [
...(auditedCollections || []),
auditCollection,
],
}
}
Поисковый плагин добавляет кастомный эндпоинт /search:
// plugins/search/index.ts
export const searchPlugin = (config: Config): Config => ({
...config,
endpoints: [
...(config.endpoints || []),
{
path: '/search',
method: 'get' as const,
handler: async (req: any, res: any) => {
const { q } = req.query
if (!q) return res.json({ docs: [] })
const results = await Promise.all([
req.payload.find({
collection: 'posts',
where: { or: [{ title: { like: q } }, { excerpt: { like: q } }] },
limit: 5,
}),
req.payload.find({
collection: 'products',
where: { name: { like: q } },
limit: 5,
}),
])
return res.json({
docs: [
...results[0].docs.map(d => ({ ...d, _type: 'post' })),
...results[1].docs.map(d => ({ ...d, _type: 'product' })),
],
})
},
},
],
})
Публикация плагина как npm-пакета
// package.json плагина
{
"name": "@myorg/payload-plugin-seo",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"peerDependencies": {
"payload": "^2.0.0"
},
"scripts": {
"build": "tsc"
}
}
Экспорт плагина из src/index.ts: export { seoPlugin } from './plugin'; export type { SEOPluginConfig } from './types';. После сборки публикуйте пакет в npm. Документация в README обязательна.
Сравнение типов плагинов
| Тип плагина | Назначение | Сложность | Пример использования |
|---|---|---|---|
| SEO | Добавление мета-полей | Низкая | seoPlugin({ collections: ['posts'] }) |
| Аудит | Логирование изменений | Средняя | auditLogPlugin({ collections: ['posts'] }) |
| Поиск | Кастомный эндпоинт | Высокая | searchPlugin() |
Типы хуков, используемых в плагинах
| Хук | Назначение | Пример в плагине |
|---|---|---|
| beforeChange | Валидация перед сохранением | Проверка уникальности slug |
| afterChange | Логирование изменений | Плагин аудита |
| beforeRead | Модификация данных перед чтением | Автозаполнение мета-полей |
| afterRead | Постобработка | SEO-плагин (добавление мета-заголовка) |
Процесс разработки плагина
- Анализ требований: определяем коллекции, хуки и эндпоинты. Учитываем возможные коллизии с существующими полями.
- Проектирование интерфейса: создаём TypeScript-типы для конфигурации плагина. Используем строгую типизацию, чтобы избежать ошибок на этапе компиляции.
- Реализация: пишем код плагина, используем глобальные хуки для централизованных изменений. Код покрываем unit-тестами (Jest) с покрытием не менее 90%.
- Тестирование: проверяем критичные сценарии, включая граничные случаи (пустые коллекции, отсутствие хуков). Также проводим интеграционное тестирование с реальной Payload CMS.
- Публикация и документация: готовим README с примерами использования, собираем TypeScript в dist и публикуем в npm. Весь процесс занимает от 3 до 10 дней в зависимости от сложности.
Что входит в работу
- Исходный код плагина на TypeScript с полной типизацией.
- Unit-тесты (Jest) с покрытием не менее 90%.
- Документация (README) с примерами конфигурации и использования.
- Поддержка после внедрения: исправления и доработки в течение месяца.
Типичные ошибки при разработке плагинов
- Не проверять, что коллекции и хуки могут быть undefined или пустыми — вызывают ошибки при запуске.
- Использовать хук afterInit для добавления полей вместо модификации коллекций напрямую — после инициализации поля уже не применить.
- Забывать экспортировать типы конфигурации плагина — пользователи не смогут получить автодополнение в IDE.
Когда заказывают разработку плагина?
Каждый наш плагин тестируется и документируется. Мы находим оптимальные архитектурные решения, учитывая специфику вашего проекта. Плагин окупается уже через несколько месяцев использования — экономия времени на добавлении однотипных полей достигает 40%. Если вы хотите получить готовое решение с гарантией качества, закажите разработку плагина — свяжитесь с нами для обсуждения задачи.







