@agentmarketing/payload-site-forms
v1.2.0
Published
Payload 3 plugin: Site Forms, submissions, form sequences (draft/save progress), and mailing. CLI (init) scaffolds UI, Site Form block, and editable mail-templates into your repo.
Readme
Payload Site Forms
A full Site Forms layer for Payload 3: reusable form definitions in the admin, submissions in the database, optional multi-step sequences with saved progress and staff approval, a front-end block, and mailing hooks — delivered without touching Payload’s official plugin-form-builder (they can coexist for legacy sites).
By Shane Farmer / Agent Marketing
Questions or issues? Email [email protected].
The model at a glance
Every Site Form document is a sequence of steps. A top-level Multi Form checkbox in the admin controls whether the sequence may contain more than one step.
| Multi Form | Meaning |
| --- | --- |
| Off (default) | Exactly one step. The step itself can be either a single page of fields or a multi-page flow. Submits in one go via POST /api/site-form-submissions. No drafts. |
| On | Two or more steps (rendered as tabs). Each step is independent and may itself be multi-page. Supports Draft safe answers, staff approval gates per step, identifier-based resume, and pipeline runs. |
Each step has its own stepType:
- Single page (
standard) — a flat list of fields. - Multi-page (
multi_step) — pages of fields with Next / Previous / Submit.
That means from the editor’s point of view there is just one form type (a sequence), driven by two checkboxes (Multi Form on the document, Step layout on each step). From the runtime point of view there is one renderer (SiteFormBlock), fed by a single normalized shape.
How the data stays backwards-compatible
Older databases may contain documents with formType: 'standard', formType: 'multi_step' (legacy flat fields / stages fields), or formType: 'multi_form' with a relationship-based pipeline array. All of these:
- Remain fully editable in the admin — legacy fields are shown only when the document is already legacy.
- Render at runtime via an adapter (
normalizeSiteForm) that projects them into the same step sequence shape the new model uses.
No migration is required to use the plugin on an existing dataset.
How it fits a Payload app
- You register
siteFormsPlugin(...)in your Payloadpluginsarray. The plugin merges collections (site-forms,site-form-submissions,site-form-draft-submissions), fields, hooks, and endpoints into your config. - Editors manage forms under Site Forms in the admin. Submissions are stored under Site Form Submissions; sequence progress lives on Site Form Draft Submissions.
- You expose the Site Form layout block on your pages (and/or posts). The block’s front-end renderer (
SiteFormBlock) reads the document, normalises it, and dispatches to either the single-step renderer or the multi-step sequence pipeline — all inside one file. - The
payload-site-formsCLI is the supported way to wire a new app.payload-site-forms initcopies the UI helpers, the block, the mail templates, and patchessrc/plugins/index.ts,next.config.js,src/collections/Pages, andsrc/blocks/RenderBlocks.tsx.
Payload 3, Next.js App Router, and @payloadcms/richtext-lexical are assumed.
Install
pnpm add @agentmarketing/payload-site-forms
# or: npm install @agentmarketing/payload-site-formsBumping to a newer release:
pnpm update @agentmarketing/payload-site-forms
# or pin a major: edit package.json to "^1.x" and run pnpm installAfter any update, run pnpm payload generate:types so your TypeScript collection types match the plugin schema.
Quick setup
1. Scaffold
From the project root:
pnpm exec payload-site-forms init
# or: npx payload-site-forms initThis copies:
src/payload-site-forms/*— field registry and React helpers (React Hook Form integration, identifier helpers, draft hook, submission serializers).src/blocks/SiteForm/*— the Site Form layout block (slugsiteFormBlock, relates tosite-forms) and its single-file rendererComponent.tsx.src/components/ui/*— shadcn-flavour primitives the scaffold imports:button,tabs,radio-group,checkbox,label,input,textarea,select. Already have a shadcn install?initskips any primitive that already exists in your project.src/components/Recaptcha.tsx— a self-contained reCAPTCHA v3 wrapper. No-ops whenNEXT_PUBLIC_RECAPTCHA_KEYis not set, so safe to keep without a reCAPTCHA account.src/lib/utils.ts— thecn()helper used by the primitives.mail-templates/site-forms/*— editable HTML / TXT copies of the bundled mail templates (commit these in your repo).
After init you'll see a one-liner command to install the Radix + CVA peers that the primitives need:
pnpm add @radix-ui/react-checkbox @radix-ui/react-label @radix-ui/react-radio-group \
@radix-ui/react-select @radix-ui/react-slot @radix-ui/react-tabs \
class-variance-authority clsx lucide-react tailwind-mergeRe-running is safe — pnpm add is a no-op for packages you already have.
…and patches:
src/plugins/index.ts— registerssiteFormsPlugin({ mailTemplatesPath: ... }).next.config.js— adds the package totranspilePackages.src/collections/Pages/index.ts— registersSiteFormBlockalongside the defaultFormBlock.src/blocks/RenderBlocks.tsx— maps thesiteFormBlockslug toSiteFormBlock.
Existing projects upgrading from a pre-1.0 scaffold: init also removes the now-obsolete src/blocks/SiteForm/MultiFormPipeline.tsx (backed up to .bak) because the single-step and multi-step renderers are consolidated into Component.tsx.
If you prefer to register the plugin by hand, the minimal wiring is:
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import { siteFormsPlugin } from '@agentmarketing/payload-site-forms'
const projectRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../..')
export const plugins = [
siteFormsPlugin({
mailTemplatesPath: path.join(projectRoot, 'mail-templates', 'site-forms'),
}),
]2. Regenerate Payload artifacts
pnpm payload generate:types
pnpm payload generate:importmap3. Create a form and add it to a page
In the admin: Site Forms → New. Author your step(s), tick Multi Form if you need more than one step, fill the Confirmation tab and Mailing tab, then add the Site Form block to a page or post and select the form.
The editor experience
Content tab
- Title / Slug — standard.
- Multi Form — unticked by default. Turn on to author a sequence.
- Draft safe answers (Multi Form only) — enable visitor draft saving for this sequence. Requires at least one identifier field on one of the steps.
- When to save answers (Draft safe answers only) —
- On submit (default) — the visitor uses a "Save draft" button.
- After each page — snapshot after every step / page navigation.
- When they leave each field — snapshot on blur.
- Steps — array of steps in the order visitors see them.
- Step label (Multi Form only) — shown as a tab heading.
- Step layout — Single page or Multi-page.
- Fields (single-page steps) or Pages (multi-page steps).
- Visitor must finish this step before the next (Multi Form only).
- Needs staff approval before continuing (Multi Form only) — blocks progression until an admin approves the step.
When Multi Form is off, the editor UI collapses to a single-form experience:
- The "Steps" label and the Add Step button are hidden — there is always exactly one step.
- The per-step Step label field is hidden.
- The Identifier checkbox on each field block is hidden (identifiers are only meaningful for sequences with draft saving).
Tick Multi Form to reveal all of the above and author a full sequence.
Confirmation tab
- Confirmation type — message or redirect.
- Confirmation message — Lexical rich text shown after successful submission.
- Redirect URL — used when confirmation type is redirect.
- Submit button label.
- Sequence awaiting-approval message (Multi Form only) — displayed while a step is pending staff review.
- Sequence rejected message (Multi Form only) — displayed when a step is rejected.
Mailing tab
Recipients, subjects, and Lexical body fields for each mail type (submission, approval request, approval approved, approval rejected). Bodies populate the {{submissionMessage}} / {{submissionMessagePlain}} merge tags inside the HTML / TXT templates.
The front-end renderer
SiteFormBlock is a single consolidated React component exposed from src/blocks/SiteForm/Component.tsx. It:
- Reads the Site Form document from the block props.
- Runs it through
normalizeSiteForm(doc)which returns aNormalizedSiteForm(unified steps array plus policy-corrected flags such asisSingleStep,allowPreSave,saveStrategy). - Dispatches to one of two internal components in the same file:
- Single-step path — used for any
isSingleStepresult (new Multi Form-off docs, legacystandard, legacymulti_step). Submits directly to/api/site-form-submissions. No pipeline runs, no draft saves. - Multi-step sequence path — used for any sequence with more than one step. Drives the tab strip, pipeline runs, identifier gate, per-step approval, and draft progress.
- Single-step path — used for any
There is no second renderer file to keep in sync. If you are adjusting layout, styling, or submit behaviour, you only ever edit Component.tsx.
Consuming the adapter yourself
If you build your own block, you can use the adapter directly:
import {
normalizeSiteForm,
type NormalizedSiteForm,
} from '@agentmarketing/payload-site-forms/normalize-site-form'
const form: NormalizedSiteForm = normalizeSiteForm(doc)
if (form.isSingleStep) {
// render form.steps[0]
} else {
// render form.steps as a sequence
}Client-only subpaths
The main package entry pulls in server-oriented code. For client components (or a thin src/payload-site-forms/index.ts re-export) import the subpaths:
import {
resolveInlineSteps,
type InlineSequenceStep,
} from '@agentmarketing/payload-site-forms/resolve-sequence-steps'
import {
normalizeSiteForm,
type NormalizedSiteForm,
type NormalizedStep,
type SiteFormLike,
} from '@agentmarketing/payload-site-forms/normalize-site-form'siteFormsPlugin, createDefaultFieldBlocks, and the mailing helpers are server only.
Submissions and sequences
site-form-submissions— one row per completed submission. Single-step forms write directly here. Sequences write the final consolidated submission here when all steps are complete.site-form-draft-submissions— one row per in-progress sequence session. Holds the pipeline run, per-step snapshots, approval status, and the identifier fingerprint used by the resume lookup.- Endpoints exposed by the plugin:
POST /api/site-form-draft-submissions/multi-form-action—ensure_run,save_progress,submit_stagefor sequences.POST /api/site-form-submissions/resume-lookup— identifier-based resume.POST /api/site-form-draft-submissions/draft-upsert— used by the draft hook.
Layout lives in mail-templates/*.html (and optional .txt). After payload-site-forms init, these are copied to mail-templates/site-forms in your project and mailTemplatesPath is patched in automatically. To add or reset templates later, run payload-site-forms mail-templates.
Message copy is pulled from the CMS Mailing tab — if a body field is empty, the plugin falls back to a sensible default. Available merge tags include {{formTitle}}, {{submissionId}}, {{pipelineRunId}}, submitter {{email}}, and any submission field name.
To bypass the file + CMS pipeline entirely, pass renderEmailTemplate to the plugin: an async function that returns { subject, html?, text? }.
Plugin options
| Option | Description |
| --- | --- |
| fieldBlocks | Custom Payload blocks for form fields (defaults provided if omitted). |
| additionalFormTypeOptions | Extra { label, value } entries for the legacy form-type select. Hidden by default; retained so custom form types still round-trip. |
| overrides.siteForms | Deep partial override for the site-forms collection. |
| overrides.siteFormSubmissions | Partial override for site-form-submissions. |
| overrides.siteFormDraftSubmissions | Partial override for site-form-draft-submissions. |
| overrides.siteFormPipelineRuns | Deprecated alias for the draft-submissions override. |
| uploadCollectionSlug | Upload relation collection slug (default media). |
| mailTemplatesPath | Directory containing {key}.html / {key}.txt; defaults to the package mail-templates/. |
| lexicalEditorForMailing | Lexical editor instance for mailing-related rich fields. |
| renderEmailTemplate | Async override: return { subject, html?, text? } and skip the file + CMS body pipeline. |
Public API (summary)
Server:
import {
siteFormsPlugin,
createDefaultFieldBlocks,
lexicalJsonToPlainText,
applyMergeTags,
} from '@agentmarketing/payload-site-forms'Client-safe subpaths:
import {
resolveInlineSteps,
type InlineSequenceStep,
} from '@agentmarketing/payload-site-forms/resolve-sequence-steps'
import {
normalizeSiteForm,
type NormalizedSiteForm,
type NormalizedStep,
type SiteFormLike,
} from '@agentmarketing/payload-site-forms/normalize-site-form'Requirements
- Payload
^3.0.0 @payloadcms/richtext-lexical^3.0.0(peer)@payloadcms/ui^3.0.0(peer — used by the admin UI helper component that gates the single-form vs. sequence editor UX)react^18 || ^19(peer)- Next.js App Router
License
MIT.
