Розробка плагінів для Payload CMS
Уявіть: у вас 10 колекцій, в кожну потрібно додати однакові SEO-поля — мета-заголовок, опис, зображення та прапорець noIndex. Ручне додавання займає 2-3 години, але при зміні структури поля доведеться правити кожну колекцію — це 20-30 годин на підтримку на рік. Плагіни Payload CMS вирішують цю проблему раз і назавжди: ви описуєте поля один раз у коді плагіна, а потім підключаєте його до потрібних колекцій одним рядком. Ми розробляємо такі плагіни під ключ з гарантією якості та підтримкою. Наш досвід — понад 5 років і 15+ плагінів для різних проєктів. Якщо вам потрібен плагін, зв'яжіться з нами для консультації.
Чому плагіни вигідні?
Основна проблема — дублювання коду. Якщо функціональність потрібна в кількох колекціях, копіювання полів і хуків роздуває базу коду. Друга проблема — складність підтримки: зміна логіки вимагає правки в кожному місці. Третя — відсутність єдиного інтерфейсу для схожих операцій. Плагіни централізують функціональність: SEO, аудит, пошук, кастомні ендпоінти. Наприклад, плагін SEO додає однакові мета-поля у всі вказані колекції, а плагін аудиту протоколює всі зміни в єдину колекцію логів. Порівняння з ручним додаванням: плагін у 3-4 рази швидше в підтримці та менш схильний до помилок. Впровадження скорочує час на 30-40%. На одному з наших проєктів для інтернет-магазину з 30 колекціями ми розробили плагін SEO, який дозволив додати мета-поля одним рядком коду, скоротивши час на 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%. Якщо ви хочете отримати готове рішення з гарантією якості, замовте розробку плагіна — зв'яжіться з нами для обговорення завдання.







