פיתוח תוספים ל-Payload CMS: ארכיטקטורה ויישום

כאשר יש צורך בפונקציונליות על פני מספר collections, שכפול קוד ועריכות ידניות הופכים את התחזוקה למטלה. אנו מפתחים פלאגינים עבור Payload CMS שמרכזים את הלוגיקה ומבטלים שכפול. הצוות שלנו מספק פרויקטים סוהריים—מהארכיטקטורה ועד היישום—ומבטיח ביצועים יציבים ותמיכה מתמשכת.

פיתוח ותחזוקה של כל סוגי האתרים:

אתרי מידע או יישומי אינטרנט
אתרי תדמית, דפי נחיתה, אתרי חברה, קטלוגים מקוונים, חידונים, אתרי קידום, בלוגים, מקורות חדשות, פורטלי מידע, פורומים, אגרגטורים
אתרי מסחר אלקטרוני או יישומי אינטרנט
חנויות מקוונות, פורטלי B2B, שווקים, בורסות מקוונות, אתרי קאשבק, בורסות, פלטפורמות דרופשיפינג, מנתחי מוצרים
יישומי אינטרנט לניהול תהליכים עסקיים
מערכות CRM, מערכות ERP, פורטלים ארגוניים, מערכות ניהול ייצור, מנתחי מידע
אתרי שירות אלקטרוני או יישומי אינטרנט
פלטפורמות מודעות, בתי ספר מקוונים, בתי קולנוע מקוונים, בוני אתרים, פורטלים לשירותים אלקטרוניים, פלטפורמות אירוח וידאו, פורטלים נושאיים

אלה רק חלק מהסוגים הטכניים של אתרים שאנו עובדים איתם, ולכל אחד מהם יכולים להיות מאפיינים ופונקציונליות ספציפיים משלו, וכן ניתן להתאים אותם לצרכים ולמטרות הספציפיים של הלקוח.

השירותים שאנו מציעים
מציג 1 מתוך 1כל 2062 השירותים
פיתוח תוספים ל-Payload CMS: ארכיטקטורה ויישום
מורכב
~3-5 ימים

הכישורים שלנו:

שאלות נפוצות

העבודות האחרונות

  • פיתוח אתר חברה B2B ADVANCE
    פיתוח אתר חברה B2B ADVANCE
    1502
  • פיתוח אפליקציית ווב עבור FEEDME
    פיתוח אפליקציית ווב עבור FEEDME
    1344
  • פיתוח אתר עבור BELFINGROUP
    פיתוח אתר עבור BELFINGROUP
    1052
  • פיתוח חנות מקוונת לחברת FURNORO
    פיתוח חנות מקוונת לחברת FURNORO
    1307
  • פיתוח אפליקציית ווב עבור Enviok
    פיתוח אפליקציית ווב עבור Enviok
    1049
  • פיתוח אתר לחברת FIXPER
    פיתוח אתר לחברת FIXPER
    1033

פיתוח תוספים ל-Payload CMS

דמיינו שיש לכם 10 אוספים, וכל אחד מהם צריך את אותם שדות SEO—כותרת מטא, תיאור, תמונה ודגל noIndex. הוספה ידנית אורכת 2-3 שעות לכל אוסף, ותחזוקה יכולה לעלות 1,000 דולר בשנה עבור פרויקט עם 10 אוספים. תוספים של Payload CMS פותרים בעיה זו אחת ולתמיד: אתם מתארים את השדות פעם אחת בקוד התוסף, ואז מצמידים אותו לאוספים הנדרשים בשורה אחת. אנו מפתחים תוספים כאלה במפתח מלא עם אחריות איכות ותמיכה. הניסיון שלנו: למעלה מ-5 שנים ו-15+ תוספים לפרויקטים שונים. אם אתם צריכים תוסף, צרו קשר לייעוץ.

למה תוספים מועילים

הבעיה המרכזית היא כפילות קוד. אם פונקציונליות נדרשת במספר אוספים, העתקת שדות ו-hooks מנפחת את בסיס הקוד. הבעיה השנייה היא מורכבות התחזוקה: שינוי לוגיקה דורש עריכות בכל מקום. השלישית היא היעדר ממשק אחיד לפעולות דומות. תוספים מרכזים פונקציונליות: SEO, ביקורת, חיפוש, נקודות קצה מותאמות אישית. לדוגמה, תוסף SEO מוסיף שדות מטא זהים לכל האוספים שצוינו, בעוד תוסף ביקורת מתעד את כל השינויים באוסף יומן יחיד. תוספים מהירים פי 3 ליישום מאשר קידוד ידני ומפחיתים את זמן הפיתוח ב-40%. עלויות היישום מוחזרות תוך חודשים בשל תחזוקה מופחתת. השוואה להוספה ידנית: תוסף מהיר פי 3-4 לתחזוקה ופחות מועד לשגיאות.

כיצד לפתח תוסף עבור Payload CMS

לפי התיעוד הרשמי של Payload CMS, תוסף הוא "פונקציה שמקבלת קונפיגורציה ומחזירה קונפיגורציה שונה." אין כאן קסם: התוסף פשוט מוסיף אוספים, שדות, hooks, נקודות קצה ורכיבים לקונפיגורציה הקיימת לפני אתחול ה-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, ביקורת, חיפוש

ארכיטקטורת התוספים של Payload פשוטה אך חזקה. תוסף 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: // Тип плагина 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], }) . לאחר בנייה, פרסמו ל-npm. תיעוד ב-README הוא חובה.

השוואת סוגי תוספים

סוג תוסף מטרה מורכבות דוגמת שימוש
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, ], } }
חיפוש נקודת קצה מותאמת אישית גבוהה // 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' })), ], }) }, }, ], })

סוגי hooks המשמשים בתוספים

Hook מטרה דוגמה בתוסף
beforeChange אימות לפני שמירה בדיקת ייחודיות slug
afterChange תיעוד שינויים תוסף ביקורת
beforeRead שינוי נתונים לפני קריאה מילוי אוטומטי של שדות מטא
afterRead עיבוד לאחר קריאה תוסף SEO (הוספת כותרת מטא)

תהליך פיתוח תוסף

  1. ניתוח דרישות: קביעת אוספים, hooks ונקודות קצה. התחשבות בהתנגשויות פוטנציאליות עם שדות קיימים.
  2. עיצוב ממשק: יצירת טיפוסי TypeScript לקונפיגורציית התוסף. שימוש בטיפוסים קפדניים למניעת שגיאות בזמן קומפילציה.
  3. יישום: כתיבת קוד התוסף, שימוש ב-hooks גלובליים לשינויים מרכזיים. כיסוי הקוד בבדיקות יחידה (Jest) בכיסוי של ≥90%.
  4. בדיקות: בדיקת תרחישים קריטיים, כולל מקרי קצה (אוספים ריקים, hooks חסרים). ביצוע גם בדיקות אינטגרציה עם מופע Payload CMS אמיתי.
  5. פרסום ותיעוד: הכנת README עם דוגמאות שימוש, קומפילציה של TypeScript ל-dist, ופרסום ל-npm. התהליך כולו אורך בין 3 ל-10 ימים בהתאם למורכבות.

מה כלול בעבודה

  • קוד מקור של התוסף ב-TypeScript עם טיפוסים מלאים.
  • בדיקות יחידה (Jest) עם כיסוי של ≥90%.
  • תיעוד (README) עם דוגמאות קונפיגורציה ושימוש.
  • תמיכה לאחר פריסה: תיקונים והתאמות למשך חודש.

טעויות נפוצות בפיתוח תוספים

  • אי-בדיקה שאוספים ו-hooks עשויים להיות undefined או ריקים — גורם לשגיאות זמן ריצה.
  • שימוש ב-hook afterInit להוספת שדות במקום שינוי ישיר של אוספים — לא ניתן להחיל שדות לאחר אתחול.
  • שכחת ייצוא טיפוסי קונפיגורציית התוסף — משתמשים מאבדים השלמה אוטומטית ב-IDE שלהם.

מתי להזמין תוסף?

כל תוסף שלנו נבדק ומתועד. אנו מוצאים פתרונות ארכיטקטוניים אופטימליים, תוך התחשבות במאפייני הפרויקט שלכם. תוסף מחזיר את ההשקעה תוך מספר חודשים של שימוש — חיסכון בזמן על הוספת שדות חוזרים יכול להגיע ל-40%. אם אתם רוצים פתרון מוכן עם אחריות איכות, הזמינו פיתוח תוסף — צרו קשר כדי לדון במשימה שלכם.