Variation #2Continue reading...

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.

FormContainerBlock · FormTextInput · FormTextArea · FormSelect · FormSubmitButton/api/form-submitODP · FX audience → CMS variation

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 — label

The 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} />;