Form Builder
Neither P1 nor Puck ships a native, end-user form builder. Confirmed directly, not assumed:
- Puck’s own docs, checked directly: no “Forms,” “Form Builder,” or “Submissions” section anywhere in their navigation. The closest capability is “integrate your own components with Puck,” you can build a form as a component, but there’s no dedicated form-authoring tooling shipped.
- P1’s own SDK packages (
css-client,puck-css,p1-next-sdk): noFormBuilder,form-builder,FormField, orwebformreference anywhere in the installed source.
This is a P1 platform gap, not a Brown-Forman-specific one: every P1 site eventually needs ad hoc forms. Filed here at the top level rather than under Brown-Forman for that reason, alongside Email Delivery, Localization, and Managing the Header & Footer. Submission storage (durably persisting every entry before a CRM or email is even attempted) is a subpage of this one: see Submission Storage.
This is a different gap from Brown-Forman’s Web Forms & Salesforce. That page covers three specific, known forms (Contact/Subscribe/Unsubscribe) we’ve already reverse-engineered from a live brand site; those get hand-built Puck components with fixed fields. This page is about the gap underneath that: what happens when a site’s marketing team wants a new form next quarter (a giveaway entry, an event RSVP, a survey) that nobody wrote a component for.
The gap
Every P1 site we build will eventually need ad hoc forms beyond whatever fixed set a project starts with. Without a form builder, each new form is a developer task: a new Puck component, new fields, new submission wiring. For a single site that’s manageable. Across many sites (every Brown-Forman brand, or any future multi-site engagement) it doesn’t scale, and rebuilding the same thing per site is exactly what this page exists to avoid.
Approach: one configurable Puck component, not a full builder
Rather than build a general-purpose visual form-builder tool (a real product in its own
right, well beyond what one component should attempt), the plan is a single Puck
component whose fields are themselves editor-configurable, reusing the array field
type exactly the way it’s already documented in
Fields Reference:
export const formBuilderBlock = {
label: "Form",
fields: {
heading: { type: "text", label: "Form heading" },
fields: {
type: "array",
label: "Fields",
min: 1,
max: 12,
getItemSummary: (item) => item.label || "Field",
arrayFields: {
label: { type: "text", label: "Field label" },
name: { type: "text", label: "Field name (used in submission payload)" },
type: {
type: "select",
label: "Input type",
options: [
{ label: "Text", value: "text" },
{ label: "Email", value: "email" },
{ label: "Select", value: "select" },
{ label: "Checkbox", value: "checkbox" },
],
},
required: { type: "radio", label: "Required", options: [
{ label: "Yes", value: true },
{ label: "No", value: false },
]},
// select-only; ignored by other types
options: { type: "textarea", label: "Options (one per line, select type only)" },
},
},
submitLabel: { type: "text", label: "Submit button label" },
destination: {
type: "select",
label: "Where responses go",
options: [
{ label: "Salesforce (via @brown-forman/p1-salesforce-forms)", value: "salesforce" },
{ label: "Email notification", value: "email" },
{ label: "Generic webhook", value: "webhook" },
],
},
},
render: FormBuilderRender, // renders fields dynamically from the array, posts to /api/forms/submit
};A site’s marketing team builds a new form entirely inside the Puck editor: no developer involved for the common case (text/email/select/checkbox fields, a destination). A developer only gets pulled in for something genuinely custom (file uploads, multi-step forms, conditional field logic) that the generic component doesn’t cover.
Config shape
interface FormBuilderConfig {
/** Reuses the Salesforce client from @brown-forman/p1-salesforce-forms when destination="salesforce". */
salesforce?: SalesforceFormsConfig;
emailDestination?: { to: string; fromAddress: string };
webhookAllowlist?: string[]; // destination URLs an editor is allowed to pick, not arbitrary input
turnstileSiteKey: string;
turnstileSecretKey: string;
}webhookAllowlist matters: letting a content editor type an arbitrary destination URL
for a generic webhook is a server-side request forgery risk if the submit handler then
POSTs to whatever URL is stored. Constrain it to a pre-approved list set by a
developer, not free text in the Puck field.
“Can we define a form once and place the same instance on multiple pages?”
A real question worth answering precisely: does P1/Puck support something like a WordPress Custom Post Type: define a form once as its own entity, then reference that same instance across many pages, so editing it in one place updates everywhere it’s placed?
No. Confirmed by the same investigation behind
Cocktails Template
and generalized on Content Relationships: every Puck
component instance’s data lives inside the content[] array of the specific page
document that contains it. There’s no P1 concept of a standalone, referenceable
content entity that multiple pages transclude; no document-picker field type
shipped anywhere in the SDK, confirmed by the same search that came up empty there.
Given that, the plan already documented above is the right call, not a fallback:
one configurable Puck component per page it appears on, using the array-field
approach. If a form’s fields need to change everywhere it’s used, that’s a multi-page
edit either way, the same tradeoff a WordPress reusable block would have without a
CPT-style backing entity behind it too.
If this becomes a real pain point later (the same form genuinely needs to update in
N places from one edit), the buildable-but-not-native pattern from
Managing the Header & Footer applies here too: store the form as
its own document at a reserved path (e.g. /_forms/newsletter-signup), and build a
custom field that lets an editor pick from existing /_forms/* documents rather than
authoring fields inline. Not proposing this now; flagging it as the known escape
hatch if per-page duplication turns out to actually hurt in practice.
Relationship to the other packages
Proposed as @prolific-digital/p1-form-builder, scoped away from @brown-forman
deliberately, since a generic form builder isn’t a Brown-Forman-specific concern, it’s
general P1 tooling any site can use. It’s not fully standalone, though: on a
Brown-Forman brand site, when destination: "salesforce" is selected it calls into
@brown-forman/p1-salesforce-forms rather than
reimplementing CSRF/Turnstile/Salesforce-proxy logic a second time, using the same
“small, focused packages that compose” approach used throughout this documentation,
not a monolith.
Whatever destination is selected, every submission from this component should also
be written to durable storage first: see
Submission Storage and
Email Delivery, so a downstream CRM or email-delivery outage
never means a lost submission.