# Conventions

URL: /docs/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.

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:

```ts title="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:

```ts
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:

```ts
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 it* — `collections: { 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:

| Key                                   | How it merges                                                                                                                                                                                         |
| ------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `slug`                                | **Renames** the collection (a direct key on the entry, alongside `overrides`).                                                                                                                        |
| `fields`                              | **Appended** 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`. |
| `hooks`                               | **Merged per phase.** Yours run after the plugin's.                                                                                                                                                   |
| `access`, `admin`, `upload`, `custom` | **Shallow-merged**, so the plugin's other keys survive.                                                                                                                                               |
| `defaultPopulate`, `forceSelect`      | **Merged** as selects.                                                                                                                                                                                |
| everything else                       | **Replaced.**                                                                                                                                                                                         |

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`:

```ts
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:

```bash
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:

| Plugin                                                 | Next.js             | Why                                                                                                                                                                                                                           |
| ------------------------------------------------------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| [payload-revalidate](/docs/plugins/payload-revalidate) | **Required**, `16+` | It's a Next cache tool — the whole plugin is `next/cache` plus Cache Components.                                                                                                                                              |
| [payload-images](/docs/plugins/payload-images)         | **Required**, `15+` | The transform endpoint, prewarming, and preset generation persist variants with Next's `after()`.                                                                                                                             |
| [payload-dev-tools](/docs/plugins/payload-dev-tools)   | **Required**, `15+` | The toolbar and dev pages are Next components (`next/link`, `next/navigation`, `next/headers`).                                                                                                                               |
| [payload-icons](/docs/plugins/payload-icons)           | **Partly**, `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-fonts](/docs/plugins/payload-fonts)           | **Partly**, `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-mux](/docs/plugins/payload-mux)               | **No**              | Zero `next/` imports, no `next` peer. Any Payload app.                                                                                                                                                                        |
| [payload-seed](/docs/plugins/payload-seed)             | **No**              | Zero `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.
