# Collections

URL: /docs/plugins/payload-images/collections

The collections the plugin registers, how to extend your own upload collection, and how images seed natively.

The plugin registers three collections under an **Assets** admin group: one you upload to, one
that caches what gets generated, and — because [`options.prewarm`](/docs/plugins/payload-images/prewarming)
is on by default — the hidden render-profile registry (an explicit `prewarm: false` drops it).
The upload collections accept PNG, JPEG, WebP, and AVIF. `collections.images.options.folders` is
on by default, so Payload's own hidden `payload-folders` collection is registered too.

## Collections

### `images`

The source upload, and the only one you touch. It stores the original untouched (no pre-generated
sizes) and keeps Payload's native focal point; every rendered size is produced on demand by
[the endpoint](/docs/plugins/payload-images/image-urls) and recorded in `generated-images`. Read
is **public**; writes are logged-in-admin only.

**Fields**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `file` | `upload` |  | The original image (the upload). Stored as-is; shown in relationship fields via displayPreview, and the admin list shows the 160px focal-cropped `thumbnail` preset (cap-exempt, pre-generated on save). |
| `alt` | `text` |  | Required. Screen-reader / SEO text; the admin title and list-search field. Localized when localizeAlt is on — which it is whenever your config sets localization. |
| `focalPoint` | `built-in` |  | Payload's native upload.focalPoint, surfaced by the picker below. |
| `folder` | `relationship` |  | Payload's native folder organization. Nullable, and added by default — set collections.images.options.folders: false to leave it off. |
| `focalPreview` | `ui` |  | Focal-point picker + ratio preview. Only with collections.images.options.focalUI. |
| `presetManager` | `ui` |  | The preset manager panel: preset toggles, cached-variant list, purge button, and the per-image variant cap. Only with collections.images.options.focalUI. |
| `variants` | `join` |  | Data/API join to this image's cached generated-images. Hidden in the admin (the preset manager panel lists variants itself). Only with collections.images.options.focalUI. |
| `presets` | `array` |  | Guaranteed public variants toggled onto this image — a presetTemplates name or a custom spec. Cap-exempt, pre-generated, served via ?preset=<name>. Managed by the preset manager panel; hidden in the admin. |
| `variantLimit` | `number` |  | Per-image cap on cached variants; blank uses the plugin's variantLimit default. Presets never count against it. Hidden in the admin (edited via the preset manager panel). |
| `src` | `virtual` |  | Default-width optimized URL, computed on read and hidden in admin. |
| `srcset` | `virtual` |  | Responsive srcset of optimized URLs. |
| `placeholderURL` | `virtual` |  | Tiny 32px blurred image URL (URL form, for non-React consumers). |
| `thumbnailURL` | `virtual` |  | 160px focal-cropped thumbnail URL. |
| `aspectRatio` | `virtual` |  | The render aspect ratio: the ratio the read declared (context.image.aspectRatio), else the natural one. |
| `variantVersion` | `virtual` |  | Cache-busting v= token for transform URLs (changes on file replace / focal / hotspot edits) — select it instead of the identity fields. |
| `blurHashXs … blurHashXl` | `text ×5` |  | The five stored BlurHash quality tiers (2×2 → 9×9 components), written by the upload hook. Hidden in the admin. |
| `placeholder2xl / placeholder3xl` | `text ×2` |  | The stored micro-webp placeholder tiers (32px / 64px full-frame data URIs), written by the upload hook. Hidden in the admin. |
| `placeholder` | `virtual` |  | A finished data URI focal-cropped to the declared render. Defaults to the sm tier (opaque rasters only — alpha/SVG docs skip the implicit default); a blur request (context.blur / X-Blurhash header) picks its own tier, and context: { blur: false } opts the read out (null). |
| `palette` | `json` |  | Sanity-style color palette extracted at upload: dominant/vibrant/muted (+ dark/light variants), each { background, foreground, title, population }. Hidden in the admin. |
| `hasAlpha` | `checkbox` |  | The file carries an alpha channel. |
| `isOpaque` | `checkbox` |  | No sampled pixel is actually transparent. |
| `focalSize / cropLeft … cropBottom` | `number ×5` |  | The hotspot & crop layer: hotspot circle diameter (% of the crop region's shorter side) and four non-destructive edge trims (%), edited via the focal picker. Hidden in the admin. |

The `presetManager` UI field renders the **Presets & variants** panel — the admin surface for the
`presets`, `variantLimit`, and `variants` fields above:

![The Presets & variants panel on an image doc, showing preset toggles, the cached-variant list, purge controls, and the variant limit](/screenshots/presets-n-variants.png)

**Hooks**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `generateImageMetadataBeforeChange` | `beforeChange` |  | When a save carries a new file, one decode stores the placeholder tiers (five BlurHash strings + the micro-webp data URIs), the color palette, the alpha flags, and — when the editor hasn't picked one — a saliency-based focal point, all in the same write (payload images:backfill covers pre-existing images). |
| `purgeStaleVariantsAfterChange` | `afterChange` |  | Purges this image's stale cached variants when its file or focal point changes. |
| `enqueuePrewarmAfterChange` | `afterChange` |  | Queues the imagesPrewarm job on create / file replace / focal edit. Only with prewarm on and `strategy.onUpload` (the default); best-effort — never blocks the write. |
| `generatePresetsAfterChange` | `afterChange` |  | Regenerates this image's guaranteed presets on create / file replace / focal or hotspot edits, after the stale-variant purge. |
| `purgeVariantsBeforeDelete` | `beforeDelete` |  | Purges all of this image's cached variants before the doc is deleted. |

### `generated-images`

The hidden, durable variant cache that you never touch directly. The transform endpoint writes one
upload doc here per (source, settings, focal) combination, keyed by a unique `cacheKey` and related
back to its `source`. It's an upload collection, so each generated variant is stored through whatever
storage adapter you've configured. It's surfaced on `images` via the `variants` join and purged by that
collection's change/delete hooks.

**Fields**

| Name | Type | Default | Description |
| --- | --- | --- | --- |
| `file` | `upload` |  | The generated variant image (the upload). |
| `source` | `relationship` |  | The images doc this variant derives from. Required, indexed. |
| `cacheKey` | `text` |  | Unique key for this (source, settings, focal) combo. The admin title. |
| `fit` | `text` |  | The resize fit used to generate it. |
| `format` | `text` |  | Output format. |
| `quality` | `number` |  | Encode quality. |
| `windowed` | `checkbox` |  | Which render path produced the pixels: a windowed cover crop (width + height) rather than a full-frame width-only render. The fallback picker matches on it when a zoomed hotspot makes the two differ. |
| `focalX` | `number` |  | Focal point X baked into the crop. |
| `focalY` | `number` |  | Focal point Y baked into the crop. |

**Hooks**: none. Variants are derived and disposable, so the collection carries no revalidation
hooks (busting cache tags on every cache-miss create would be pure churn).

> **Variant reads inherit the source's access policy.** `generated-images` read access requires an
> authenticated user AND defers to the source collection's read access, re-rooted through the
> `source` relationship — so a tenant-scoped or owner-only `images` policy scopes the cached
> variants (docs and bytes) automatically. Caveat: a source read access that branches on the
> individual doc `id` can't be re-rooted; in that case supply your own
> `collections: { generatedImages: { overrides: { access: { read: … } } } }`. The transform endpoint
> itself always gates on the source's read access.

### `image-render-profiles`

Registered only with [`prewarm`](/docs/plugins/payload-images/prewarming) on: the hidden
registry of render profiles the transform endpoint has actually served — one doc per distinct
`(ratio, fit, quality, format)` shape, carrying an approximate `hitCount`, a `lastSeenAt`
(profiles unseen for a while stop being warmed), and a capped per-width observation histogram.
Written only by the endpoint's buffered recorder; read by the prewarm job/CLI. Small by
construction: the endpoint's snap grid and quality buckets bound the distinct-profile space. Like
the variant cache, it carries no revalidation hooks.

Reshape it with `collections.renderProfiles`, same as the other two. `options.prewarm` is what
decides whether it exists at all — with `options: { prewarm: false }` there's no collection for the
key to shape.

## Renaming a collection, and adding your own fields

Most projects already call their upload collection `media`. Rename the one the plugin registers and
add your fields to it — `slug` is a first-class override, and the plugin repoints every internal
reference at the new name:

```ts
imagesPlugin({
  collections: {
    images: {
      slug: 'media',
      overrides: { fields: [{ name: 'credit', type: 'text' }] },
    },
  },
})
```

That registers **one** collection, `media`, carrying the whole pipeline (the focal UI, the
`variants` join, the purge hooks, `upload.focalPoint`) plus your `credit` field. The transform
endpoint serves `/api/img/:id` for `media` docs, the variant cache's `source` relationship points at
`media`, and `readImagesMarker(config).sourceSlug` reads back `'media'`.

The variant cache renames the same way:

```ts
imagesPlugin({ collections: { generatedImages: { slug: 'image-variants' } } })
```

> **`slug`, `overrides`, and `options` — one key each.** Every `collections.<name>` splits the same
> way: `slug` renames it, `overrides` is a plain `Partial<CollectionConfig>` (`upload`, `access`,
> `admin`, `custom` shallow-merge over the plugin's, `fields` and `hooks` append after them,
> `defaultPopulate` and `forceSelect` merge as selects, everything else replaces), and `options` is
> the plugin's own knobs for the collection. So a `collections.images.options.mimeTypes` /
> `maxOriginalSize` and a `collections.images.overrides.upload` both reach `upload` — set either, and
> the explicit `overrides.upload` wins. (`localizeAlt` isn't one of them: it localizes the `alt`
> field, not `upload`.)

> **Don't redeclare a field the plugin injects.** `overrides.fields` append, so a field of yours
> named `alt`, `variants`, `placeholder`, `palette`, … is a boot error naming the plugin, the
> `collections` key, and the field — rename yours instead.

## Seeding

Because `images` is a plain Payload upload collection, it seeds **natively** through
[`@pro-laico/payload-seed`](/docs/plugins/payload-seed): `defineSeed('images', …)` with each
record carrying its source file on `_file`, referenced from a page with `ref('images', …)`:

```ts
import { defineSeed, seedPlugin } from '@pro-laico/payload-seed'
import { imagesPlugin } from '@pro-laico/payload-images'

// src/seed/images.ts — focal points drive the on-demand crops from the first serve
const images = defineSeed('images', ({ file }) => [
  { _key: 'hero', _file: file('hero.jpg'), alt: 'Hero', focalX: 78, focalY: 32 },
])

// src/seed/pages.ts — reference a seeded image (an upload-field relationship)
const pages = defineSeed('pages', ({ ref }) => [
  { _key: 'home', title: 'Home', heroImage: ref('images', 'hero') },
])

plugins: [
  imagesPlugin(),
  seedPlugin({ definitions: [images, pages], options: { assetsDir: 'seed-assets' } }),
]
```

`alt`, `focalX`, and `focalY` are ordinary record fields, type-checked against the collection.
Payload's folders are on by default (`collections.images.options.folders`) and seed the same way — ordinary docs in the
hidden `payload-folders` collection, each image referencing its folder like any relationship. See
[`examples/images-sandbox`](https://github.com/pro-laico/payload-plugins) for a full working
setup, and the [payload-seed docs](/docs/plugins/payload-seed) for everything else about seeding.
