Розробка плагіна для Payload CMS

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка плагіна для Payload CMS
Складна
~3-5 робочих днів
Часті питання

Наші компетенції:

Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка плагінів для Payload CMS

Плагін Payload — це функція, яка приймає конфігурацію та повертає змінену конфігурацію. Це не магія: плагін просто додає колекції, поля, хуки, ендпоінти та компоненти до існуючої конфігурації перед ініціалізацією 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 плагін (повна реалізація)

// plugins/seo/index.ts
import type { Config, CollectionConfig, GlobalConfig } from 'payload/types'

interface SEOPluginConfig {
  collections?: string[]   // слаги колекцій, куди додати 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
        },
      ],
    },
  }
}

Використання:

// payload.config.ts
import { seoPlugin } from './plugins/seo'

export default buildConfig({
  plugins: [
    seoPlugin({
      collections: ['posts', 'pages', 'products'],
      globals: ['home-page'],
      uploadsCollection: 'media',
      generateTitle: (doc) => `${doc.title} | My Site`,
      generateDescription: (doc) => doc.excerpt || '',
    }),
  ],
})

Плагін журналу аудиту

// 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,  // тільки через API
      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,
    ],
  }
}

Плагін з користувацькими ендпоінтами

// 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'

Тестування плагіна

// tests/plugin.test.ts
import { buildConfig } from 'payload/config'
import { seoPlugin } from '../src'

describe('SEO Plugin', () => {
  it('повинен додати SEO-поля до вказаних колекцій', () => {
    const baseConfig = buildConfig({
      collections: [{ slug: 'posts', fields: [{ name: 'title', type: 'text' }] }],
      plugins: [seoPlugin({ collections: ['posts'] })],
    })

    const postsCollection = baseConfig.collections.find(c => c.slug === 'posts')
    const metaField = postsCollection?.fields.find((f: any) => f.name === 'meta')

    expect(metaField).toBeDefined()
    expect(metaField?.type).toBe('group')
  })
})

Терміни

Розробка одного переиспользуемого плагіна (SEO, аудит, пошук) з тестами займає 3–5 днів.