Developer Demo
Forms & Data Capture
CMS-managed forms built from composable blocks — editors configure fields in Visual Builder, the submit handler captures data, and each submission feeds the personalization loop: capture → ODP profile → FX audience → targeted content.
Live Form #
The form below is assembled from the five form block components — the same components an editor would drag into Visual Builder. Try submitting it and watch the network tab: it POSTs JSON to /api/form-submit.
Contact Us
Fill in the form and we'll get back to you within one business day.
How It Works #
Form blocks are independent components that cooperate via the DOM rather than a shared React state tree. This makes them composable in Visual Builder alongside any other block — a form can sit in any column, any section.
1. FormContainerBlock
Renders the heading and description. Sets data-form-submit-url and data-form-success-message as data attributes — no React context needed.
2. Field blocks
Each renders an input, textarea, or select with a derived name attribute (from the fieldName or slugified label property).
3. FormSubmitButton
A "use client" component. On click: reads data-form-submit-url, collects all inputs in the page scope, validates required fields, POSTs JSON, shows success or error state.
4. /api/form-submit
Receives the JSON payload. In production: forward to your CRM or Optimizely Data Platform as a customer event. The demo logs to console and returns { success: true }.
Submit button — DOM-scoped collection
// FormSubmitButton is a "use client" component.
// Rather than wrapping all fields in a <form>, it scans the page scope —
// this works because Visual Builder compositions are flat siblings, not nested.
async function handleClick() {
const scope = ref.current?.closest("main") ?? document.body;
const configEl = scope.querySelector("[data-form-submit-url]");
const submitUrl = configEl?.getAttribute("data-form-submit-url") ?? "/api/form-submit";
// Collect every input, textarea, and select within the same page scope
const inputs = scope.querySelectorAll("input, textarea, select");
const payload: Record<string, string> = {};
inputs.forEach((el) => { if (el.name) payload[el.name] = el.value; });
const res = await fetch(submitUrl, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
}Block Schemas #
All five block types are registered in componentRegistry.ts so editors can place them in any Visual Builder composition. The submitUrl on FormContainerBlock lets editors point different forms at different endpoints — a contact form, a newsletter signup, and a demo request can all live in the same CMS with separate handlers.
// FormContainerBlock — heading, description, submitUrl, successMessage
export const FormContainerBlockType = contentType({
key: "FormContainerBlock",
baseType: "_component",
compositionBehaviors: ["sectionEnabled", "elementEnabled"],
properties: {
heading: { type: "string", displayName: "Heading" },
description: { type: "string", displayName: "Description" },
submitUrl: { type: "url", displayName: "Submit URL" },
successMessage: { type: "string", displayName: "Success Message" },
},
});
// FormTextInput — label, placeholder, fieldName, inputType (text/email/tel), required
// FormTextArea — label, placeholder, fieldName, required
// FormSelect — label, fieldName, options (comma-separated string), required
// FormSubmitButton — labelThe Submit Handler #
The route receives a flat JSON object keyed by field name. Swap the console log for any integration — CRM, email service, or Optimizely Data Platform.
// src/app/api/form-submit/route.ts
export async function POST(request: NextRequest) {
const body = await request.json();
// body = { name: "Jane", email: "jane@...", interest: "Enterprise", message: "..." }
// Log the submission (swap for your CRM / ODP integration here)
console.log("[Form Submission]", body);
// To send to Optimizely Data Platform as a customer event:
// await fetch("https://api.zaius.com/v3/events", {
// method: "POST",
// headers: { "x-api-key": process.env.ODP_API_KEY },
// body: JSON.stringify({
// type: "form_submit",
// identifiers: { email: body.email },
// data: body,
// }),
// });
return NextResponse.json({ success: true });
}Closing the Personalization Loop #
A form submission is the beginning of a customer profile, not the end. Connect the submit handler to Optimizely Data Platform (ODP) and the submission feeds straight into Feature Experimentation audience conditions — which the CMS page route already reads to serve targeted content variations.
User submits form (email captured)
│
└─→ POST /api/form-submit
└─→ POST to ODP: { type: "form_submit", identifiers: { email }, data: payload }
└─→ ODP builds customer profile: { email, logged_in: true, ... }
Next page request (same user, identified by cookie)
└─→ FX evaluates "cms_personalization" flag
Audience: logged_in = true → variation "returning_users"
└─→ Graph returns the CMS variation built for returning users
└─→ OptimizelyComponent renders it — zero extra code// The submit → ODP → FX loop:
// 1. User submits the form (email captured in body.email)
// 2. /api/form-submit POSTs to ODP as a customer event
// ODP builds a customer profile: { email, logged_in: true, ... }
// 3. Next request: FX evaluates "cms_personalization" flag for this user
// Audience condition: logged_in = true → variation "returning_users"
// 4. [[...slug]]/page.tsx passes variation key to Graph
const [page] = await client.getContentByPath(url, {
variation: {
include: "SOME",
value: ["returning_users"],
includeOriginal: true,
},
});
// 5. Graph returns the CMS variation an editor built in Visual Builder
// specifically for logged-in / returning users
return <OptimizelyComponent content={page} />;