Payload Plugins
Plugins

Conventions

The patterns every @pro-laico plugin shares — one factory shape, zero-config defaults, and composition through plain Payload config so nothing ever imports another plugin.

For AI / LLMs: View Markdown

Every @pro-laico/* plugin is built the same way. Learn these once and each package feels familiar — same install, same shape, same escape hatches.

The factory shape

Every plugin is a (opts) => (config) => config factory, exported as both the default and a named export. Drop it into your Payload config's plugins array:

payload.config.ts
import { buildConfig } from 'payload'
import { pluginName } from '@pro-laico/<plugin>'

export default buildConfig({
  // ...
  plugins: [pluginName()],
})

Zero-config by default

Every option has a sensible default, so pluginName() with no arguments works out of the box. When you need more control, the raw building blocks — collections, hooks, fields, and admin components — are exported too, so you can wire them by hand instead of through the factory. Reach for the factory first; reach for the exports only when you outgrow it.

Defaults lean toward the feature being on. A plugin you installed should do its job without a checklist of opt-ins, so anything integral to the plugin isn't a toggle at all — payload-images always registers its transform endpoint, and payload-seed always registers its button (the ENABLE_SEED env var is the real switch there).

The options shape

Every plugin's options follow the same skeleton, so once you've configured one you can guess the next:

pluginName({
  enabled: true,        // always defaults true; false makes the plugin a no-op
  collections: { … },   // one entry per collection the plugin registers
  globals: { … },       // …and per global, where the plugin has any
  // …the one or two items that ARE the plugin (payload-seed's `definitions`)
  options: { … },       // everything else — the plugin's own knobs
})

The split at the root is the whole idea: collections / globals describe what the plugin registers, and options holds the plugin's own behavior. A plugin whose entire job is one thing keeps that thing at the root next to options (payload-seed leaves definitions there); everything else lives under options.

Inside a collection

Every collections.<name> (and globals.<name>) entry is the same three-part shape, so a collection reads the same way in every plugin:

imagesPlugin({
  collections: {
    images: {
      slug: 'media',                          // rename it
      overrides: { access: { read: … } },     // Payload passthrough — anything CollectionConfig has
      options: { folders: true, focalUI: … }, // this plugin's own knobs for THIS collection
    },
  },
})
  • slug renames the collection; the plugin follows the rename through every reference it owns — relationship and join fields, hooks, endpoints, and the slug on its marker.
  • overrides is a Partial<CollectionConfig> (or Partial<GlobalConfig>), merged onto the plugin's — not an allowlist of blessed keys. Whatever Payload's config has, you can set, except slug: it's the sibling key above, excluded here so a rename can't hide in overrides where the plugin wouldn't follow it.
  • options is the plugin's vocabulary for that collection — the admin cell, upload constraints, and so on. Kept apart from overrides so the two never bleed together.

Two more rules:

  • One entry per collection, one axis per key. Where a collection is optional, false means this plugin ignores itcollections: { iconSet: false } drops the collection payload-icons would register. A collection the plugin can't work without has no false form (there's no payload-images without images); each plugin's reference says which.
  • Optional sub-features read false | Options. No true form; {} means "on, all defaults". prewarm: {} and prewarm: false — not prewarm: true.

payload-revalidate is the one plugin that doesn't register collections — it annotates the ones you already have. So its collections is keyed by your slugs and each entry is just the tracking config (there's no slug to rename or overrides to merge), with false to opt one out: collections: { posts: { idField: 'slug' }, drafts: false }.

How overrides merges

One algorithm, every plugin, every collection:

KeyHow it merges
slugRenames the collection (a direct key on the entry, alongside overrides).
fieldsAppended after the plugin's. A field named like one the plugin injects is a boot error naming the plugin, the collections key, and the field — rather than Payload's bare DuplicateFieldName.
hooksMerged per phase. Yours run after the plugin's.
access, admin, upload, customShallow-merged, so the plugin's other keys survive.
defaultPopulate, forceSelectMerged as selects.
everything elseReplaced.

The rule of thumb: anything the plugin depends on to work is merged, never clobbered; anything else is yours. Renaming plus overrides is the only way to reshape a plugin's collection — there's no separate option for attaching a plugin's fields to a collection you declared yourself.

Reading a plugin's config back

Each plugin stashes a typed marker on config.custom.payload<Name> describing what it resolved — slugs, endpoint paths, settings. Read it with the plugin's read<Name>Marker(config) helper rather than hand-casting custom:

import { readImagesMarker } from '@pro-laico/payload-images'

const { sourceSlug, basePath } = readImagesMarker(payload.config) ?? {}

Composition without coupling

No plugin imports another. They discover each other at build time through config.custom.* markers, so any combination works and any one can be installed alone.

  • payload-seed is the connective tissue — images, icons, and font files seed natively as uploads; Mux clips seed through a custom.seedAsset marker the engine auto-discovers. One set of seed.ts files bootstraps a whole site.
  • payload-dev-tools lights up diagnostic panels for whatever it finds installed, reading each sibling's marker (payloadImages, payloadIcons, payloadFonts, payloadMux, payloadSeed, payloadRevalidate).
  • payload-revalidate picks up sibling custom.revalidate markers to know what to bust.

Composition is plain Payload config — there's no shared runtime, no plugin registry, and no install order to get right.

The Assets admin group

The asset collections — images, icons, fonts, and Mux video — share a single Assets group in the admin sidebar, so your media stays together no matter how many of these plugins you install.

When to run generate:importmap

Any plugin that registers admin components by string path needs the import map regenerated so Payload can resolve them. That covers the images focal/preset UI, the seed button, the Mux uploader, and the icons preview. After installing one of these, run:

pnpm payload generate:importmap

Then restart the dev server so the new components load.

Requirements

Payload ^3 and React 19 for every package. The packages ship in lockstep under one version, but each works standalone — you never need to install more than the one you want.

Next.js is per-plugin, not a blanket requirement:

PluginNext.jsWhy
payload-revalidateRequired, 16+It's a Next cache tool — the whole plugin is next/cache plus Cache Components.
payload-imagesRequired, 15+The transform endpoint, prewarming, and preset generation persist variants with Next's after().
payload-dev-toolsRequired, 15+The toolbar and dev pages are Next components (next/link, next/navigation, next/headers).
payload-iconsPartly, 15+The collection, admin, and SVG pipeline work in any Payload. The <Icon> component reads draftMode() and runtime miss-tracking uses after(), so rendering icons needs Next.
payload-fontsPartly, 15+Uploading, subsetting, and exporting work anywhere. The download CLI writes a next/font/local module, so the serving story is Next-shaped — without it, take the exported .woff2 files and write your own @font-face.
payload-muxNoZero next/ imports, no next peer. Any Payload app.
payload-seedNoZero next/ imports, no next peer. Any Payload app.

Each plugin's own page states this up front too.

Payload Plugins is not affiliated with Payload CMS in any capacity.

On this page