Разработка плагинов для 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%. Если вы хотите получить готовое решение с гарантией качества, закажите разработку плагина — свяжитесь с нами для обсуждения задачи.







