JJourney AI

Forms

How forms built in the panel are rendered, validated, and submitted on the site.

Updated 27 October 20265 min read

This page covers how CMS forms render on the site, how field types map to widgets, and the path a submission takes to reach the CMS.

A form in the tree

When a form is placed on a page in the panel, a container with the form slug appears in the component tree. Its children are FormItem leaves, and each leaf's data is JSON describing the field.

Form node
{
  "id": 42,                 // form component ID used for submission
  "uniqueId": 9001,
  "slug": "form",
  "componentType": "Form",
  "children": [
    {
      "title": "Email",     // field name in the submission
      "componentType": "FormItem",
      "data": "{\"type\":\"email\",\"label\":\"Email\",\"required\":true}"
    }
  ]
}

FormContainer receives this node through the registry, reads its children, builds the fields, and validates them with react-hook-form and zod.

Field definition

Every key in FormItem.data is optional. The CMS may not send all of them.

KeyDescription
typeField type. See the table below.
label, placeholderLabel and placeholder text.
descriptionHelp text under the field. Can be Markdown, such as a consent text with a link.
requiredRequired field.
optionsOptions for choice fields.
defaultValue, defaultCheckedDefault value.
min, max, stepBounds for numbers and sliders.
rowsHeight of a long text field.
acceptFile types accepted by a file field.
widthWhen "half", the field takes half a column on medium and larger screens.
i18nPartial copies per language that override the values above.

The field name in the submission is the title of the FormItem node.

Field registry

Field types map to widgets in src/components/cms/form/field-registry.ts.

TypeWidget
text, email, tel, url, password, timeTextField
numberNumberField
textareaTextareaField
selectSelectField
multiselect, checkbox-groupCheckboxGroupField
radioRadioField
checkboxCheckboxField
switchSwitchField
sliderSliderField
date, datetimeDateField
fileFileField
hiddenHiddenField

The template also accepts some aliases, such as dropdown → select, phone → tel, and toggle → switch. An unknown type falls back to TextField, so a missing widget shows up instead of crashing the page.

Adding a field type

Add one line to the registry and one file under fields/.

src/components/cms/form/field-registry.ts
import RatingField from "./fields/RatingField";

const FIELD_REGISTRY = {
  // …
  rating: RatingField,
};

Also add the new type to FIELD_TYPES in src/lib/forms/parse.ts. The registry is typed against that list.

Submission flow

The browser posts to the site's own endpoint

useFormSubmit sends the validated values to POST /api/form/{componentId}. Date values become ISO strings and empty values are dropped. The page path (pagePath) and page ID (pageId) are added to the body.

The site route forwards to the CMS

src/app/api/form/[componentId]/route.ts rejects cross-site requests and forwards the body to the CMS endpoint POST /api/v1/form/submit/{componentId}. The visitor's IP is forwarded to the CMS using SITE_PROXY_SECRET, so submission records see the real visitor.

The CMS validates and stores

The CMS checks that the form exists and validates the fields. Follow-up actions such as email or webhooks are configured per form in the panel. The site doesn't trigger them.

The result is shown to the user

On success, FORM_MESSAGES.submitSuccess is shown. On failure, the CMS errorMessage or FORM_MESSAGES.submitError is shown.

The body sent to the CMS looks like this:

POST /api/v1/form/submit/42
{
  "formData": { "Email": "[email protected]" },
  "languageId": "en",
  "pagePath": "contact",
  "pageId": 123
}

pageId determines the submission's language and page. languageId, derived from the path, is the fallback.

Form messages

All user-facing form texts live in the FORM_MESSAGES object in src/lib/template-config.ts: validation messages, button labels, and success and error texts. Edit them there instead of hard-coding them in components.

Captcha and file uploads

The delivery API offers two more endpoints:

  • GET /api/v1/form/captcha-config returns the captcha provider and site key. An empty provider means captcha is off.
  • POST /api/v1/form/upload/{componentId} uploads a file chosen by the visitor. The file path in the response is sent as the field's value on submission.

The CMS supports Google reCAPTCHA v2, reCAPTCHA v3, and Cloudflare Turnstile as captcha providers. The provider and keys are set in the Form security category of the panel's Settings screen; captcha is not a form field. If no secret key is configured, captcha verification is skipped on submission. See Settings.

The upload endpoint requires captcha verification. The default size limit is 5 MB. Allowed extensions: .pdf, .doc, .docx, .xls, .xlsx, .txt, .csv, .jpg, .jpeg, .png. Uploaded images are converted to webp.

Not yet supported in the site template

The current site template has no captcha widget, and submissions don't send a captcha token. If a captcha provider and key are configured in the panel, submissions from this template may fail verification. The template's file field doesn't upload the file itself; it writes only the file name to the body. File-type form fields are also disabled in the panel for now.

Related pages

On this page