Collections
The three collections payload-icons registers — the SVG upload pipeline, the swappable icon sets, and the runtime miss tracker.
iconsPlugin() registers three collections:
icon— the SVG upload collection, in an Assets admin group.iconSet— thename → icongrouping collection, in a Sets admin group.iconRequest— a hidden diagnostics collection, on by default (disable withcollections: { iconRequest: false }).
Icons accept SVG only. Every upload is sanitized before storage, so a .svg is safe to inline straight onto a page.
icon
The upload collection you add icons to. Each file is optimized and sanitized on save by the
formatSVGHook beforeChange hook and stored inline as svgString. Read is public (icons are
frontend assets); writes are logged-in-admin only. The admin titles each doc by filename.
Fields
Prop
Type
Hooks
Prop
Type
On save, formatSVGHook runs svgo (loaded dynamically, so it never lands in a frontend or edge
bundle) and:
- Sanitizes untrusted SVGs: strips
<script>elements,on*handlers, andjavascript:URLs. The stored string is later inlined viadangerouslySetInnerHTML, so this runs even when geometry optimization is skipped. - Optimizes with svgo (
preset-default+ path/number cleanup, dimensions removed). - Themes by rewriting hard-coded
fill/stroketocurrentColor, so an icon takes its color from CSS. - Normalizes the
viewBox: tightened to the real path bounds and squared around the glyph's center, so mismatched source artboards render consistently.
Payload blocks SVG uploads by default (they're on its restricted-file-types list). This collection
is SVG-only and sanitizes every file before storage, so it opts in automatically via
upload.allowRestrictedFileTypes, with nothing to configure.
SVGs using transform / clip-path skip the geometry rewrite (it can't be applied safely) but
are still sanitized before storage.
iconSet
A named, ordered name → icon mapping into the shared icon pool, with a single-active toggle. Build
a second set (a different arrow-right, a heavier weight, a seasonal pack) and flip it active to
re-skin every icon at once, without touching the frontend. Reads and writes require a logged-in user.
Drafts / versions are on by default; override with collections.iconSet.overrides.versions like any
other Payload collection. Lives under a Sets admin group.
- Settings tab: the
activetoggle andtitle(plus the usage panel unlesscollections.iconSet.options.usagePanel: false). - Icons tab: the
iconsArray, one row per icon, each aname(auto kebab-cased, what the frontend looks up, independent of the uploaded filename) and aniconupload relationship into theiconcollection — plus anything you add viacollections.iconSet.options.iconRowFields.
Only one set is active at a time. Activating a set runs a beforeChange hook that deactivates the
others within the same status lane, so staging a new active set as a draft (with live
preview) doesn't disturb the live published set; the swap goes live only on publish. The hook runs
in the same transaction and rolls back on failure, so you never end up with two active sets.
It's self-contained: a plain checkbox, no external dependency.
iconRequest
On by default (disable with collections: { iconRequest: false }). A hidden diagnostics collection: every icon name
that fails to resolve at runtime is recorded (throttled, fire-and-forget) with a hit count and first/lastRequestedAt
timestamps. It's the runtime counterpart to the static scan, capturing dynamic names a static pass can't
see. Surfaced in the IconSet usage panel, not browsed directly.
Icon use detection
Problem: code asks for a name the active set lacks → the fallback warning glyph, and it's easy to miss which icons a project actually needs.
Fix: the admin Requested icons panel (on by default) lists missing vs present names, from two sources:
- Static scan: greps your source for literal
<Icon name="…">, flagging missing names withfile:line. In dev the panel scans live, so there's nothing to set up or run. For production (source isn't on disk at runtime) runpayload-icons-scanin your build to write anicon-usage-manifest.jsonthe panel reads there.
The live dev scan takes the defaults: roots ./src and ./app relative to the dev server's
working directory, and a component literally named <Icon>. If your layout differs — icons under
another root, an aliased tag like <AppIcon>, or a monorepo where the server doesn't start at the
app root — it finds nothing. When that happens the panel falls back to the
icon-usage-manifest.json on disk, so running payload-icons-scan with the right --component /
roots fixes the panel in dev too. A scan that found nothing is reported as such, never as
"all present" — an empty scan is not a pass.
- Runtime: on by default too,
<Icon>logs names that fail to resolve at runtime (incl. dynamicname={slug}the scan can't see) into the hiddeniconRequestcollection, with hit counts, last-seen, and a clear button.
Both are on with iconsPlugin(); pass collections: { iconSet: { options: { usagePanel: false } } } /
collections: { iconRequest: false } to omit either.
payload-icons-scan # production/CI: scans ./src and ./app; -o sets the manifest pathScan sees literal names only: name="x", name={'x'}, name={`x`}; dynamic
name={expr} is the iconRequest recorder's job. In prod the panel reads icon-usage-manifest.json from cwd
(override with the ICON_USAGE_MANIFEST env var). Other component? --component Glyph. On
standalone / Vercel builds, a JSON file that's never imported isn't traced into the function
bundle — add it to outputFileTracingIncludes in next.config so the panel can read it there.
Two env vars tune usage detection at runtime, independent of the plugin options —
ICON_USAGE_TRACKING and ICON_USAGE_MANIFEST. See
Reference.