Support for user-generated content is coming soon.
Problem statement
You may want to collect input from your clients, readers, or users through a form in your frontend. If you need that form to match your own UI and layout, the component-rendered approach gives you more control over how the form is presented.What you are doing
You will add a schema-driven form to your frontend by rendering Publive form fields through your own component layer and handling submissions through your own frontend flow. This guide covers the extended customization path. If you want the default prebuilt flow instead, see How to integrate form. If you need hidden fields, prefilled values, or label changes, add overrides on top of the base schema (for example viaform_schema or your own schemaOverrides pattern).
What the form renderer expects
The reference app uses a shared renderer atcomponents/ui/microorganism/Form/index.tsx. Your app can follow the same contract:
form_related_field: the full reader-side form schema array (the first entry is the active form schema).form_schema: optional patch object or JSON string merged into that schema (labels, placeholders, visibility, defaults, etc.).className: optional layout classes.
- reads the first schema from
form_related_field - merges any
form_schemachanges into that base schema - renders field groups and field types from the merged schema
- validates inputs (client-side) before submit
- requires reCAPTCHA when configured
POSTsFormData(multipart) to/api/form/{formId}/submit— not JSON
Reader-side payload shape: { value } wrappers
Content Delivery often serializes scalars and some nested values as single-key objects:
meta_data (for example label, placeholder, type) and on some validations entries (for example required, choices).
Before rendering any string in React (labels, placeholders, option text) or using values in validation logic, unwrap them:
- If the value is a plain object with only a
valueproperty, usevalue.valuefor display and logic. - If you skip this, React can throw: Objects are not valid as a React child (found: object with keys ).
dynamic_fields (for example heading, description) — unwrap before putting them in JSX.
Field typing: field.type vs meta_data.type
The reader schema may give you two layers:
field.type— storage-oriented types such asshort_text,long_text,integer,email, etc.meta_data.type(often wrapped as{ value: "input" | "textarea" | "select" | ... }) — widget hint for the UI.
validations.choices as { value: ["A", "B"] }, not only under meta_data.options.
Recommended resolution order for “what to render”:
- After unwrapping, if
meta_data.typeis set, prefer it for the widget (input,textarea,select, …). - Map
field.typeto HTML primitives where needed (integer→number,short_text→text,long_text→textarea, etc.). - If
choices(unwrapped) is a non-empty array, treat the field as a select even whenfield.typeis stillshort_text.
validations.required
required may appear as:
{ value: true }, or- an empty object
{}(still meaning required in many schemas),
{} as required, treat that explicitly in your validator.
1. Get the form data from your page payload
This guide assumes your section or custom field already includes a form relation. In the reference app, the simplest pattern looks like:form_related_field and optional form_schema from that object into your renderer. For the component-rendered path, pass the API response as-is first, then add overrides only where needed.
2. Render the shared form component
Create or update the section where you want the form to appear:3. Client submit: build FormData
The CMS form submission API expects multipart/form-data with at least:
schema— same value as the form schema id (24-character hex string)g-recaptcha-response— when reCAPTCHA is enabled- One part per dynamic field, using the field
namefrom the schema (not the display label)
Content-Type manually — the browser sets the multipart boundary.
4. Add the submit API route
The renderer submits to:/api/form/[id]/submit
That route should:
- accept
POSTonly - set
bodyParser: falseso Next.js does not consume the raw multipart body - read the incoming body as
FormData - call
sdk.utils.form.submitForm(formId, formBody, { isCms: true })(use your SDK variable, e.g.sdk, not an undefinedpublive) - return the upstream response to the client
Node.js + Next.js Pages Router (recommended)
On Node,new Request(..., { body: req as any }) is not reliable. Convert the Node request stream to a Web ReadableStream with Readable.toWeb(req) and pass duplex: "half" when constructing Request, then await request.formData().
Also build the request URL with Host and X-Forwarded-Proto when req.url is only a path (proxies, production):
Error responses on the client
Success and error payloads from the CMS differ. Your UI may need to read nested paths (for exampleresponse.response.message or error.description) depending on what your API route forwards — see Form submission.
5. reCAPTCHA keys
- Site key (browser): the reader form schema may include
captcha_config.site_key. The reference app also supportsNEXT_PUBLIC_RECAPTCHA_SITE_KEYas a fallback when the schema does not carry a key. - Secret (server): verify the token server-side before or alongside forwarding to Publive; do not rely on the client alone.
6. Verify the flow
Check the full path locally:- Open a page that contains the form relation.
- Confirm fields render with labels and validation (after any
{ value }unwrapping). - Complete reCAPTCHA when required.
- Submit the form.
- Confirm the browser sends multipart
POSTto/api/form/{id}/submit(notapplication/json). - Confirm you see the success state after a
200or201response from your route.
When to use this approach
Use the component-rendered approach when you need more than the default iframe flow. Typical cases are:- injecting hidden fields
- pre-filling defaults from page data
- changing the visible form title or description
- applying page-specific field overrides
- matching your design system closely
- controlling where and how the form is rendered in your layout
form_schema / schemaOverrides) in addition to form_related_field.
Integration checklist
-
form_related_fieldis a non-empty array; first element hasidandfield_types. - All user-visible strings from
meta_data/ validations are unwrapped from{ value }before JSX. - Widget type considers
meta_data.typeandvalidations.choicesas well asfield.type. - Client submits
FormDatawithschema,g-recaptcha-response(if used), and fieldnamekeys. - API route:
bodyParser: false,FormDataparsing compatible with Node (Readable.toWeb),sdk.utils.form.submitForm,{ isCms: true }. - reCAPTCHA secret verified server-side where applicable.