Разработка плагина для 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[]   // 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
    }),
    // Добавить хук для auto-populate
    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 дней.