Built-in types
Every field shares these keys:
{ name: string; // the object key. No dots label: string; // shown above the control help?: string; // shown under it tab?: string; // which tab of a multi-tab form it belongs to ai?: FieldAI; // the copy assistant's descriptor}Every type
Section titled “Every type”type |
Stores | Schema to pair with | AI-editable |
|---|---|---|---|
text |
string |
text, required |
yes |
url |
string |
link, requiredLink |
no |
email |
string |
text.email() |
no |
tel |
string |
text |
no |
textarea |
string |
text |
yes |
richtext |
string of HTML |
text |
yes |
image |
string URL |
link |
yes |
avatar |
string URL |
link |
no |
select |
string |
z.enum([...]) |
yes |
toggle |
boolean |
toggle, enabled |
yes |
stringlist |
string[] |
z.array(required) |
yes |
group |
object |
z.object({...}) |
yes |
list |
object[] |
z.array(z.object({...})) |
yes |
seoPreview |
nothing | none | no |
Per-type options
Section titled “Per-type options”text, url, email, tel
Section titled “text, url, email, tel”No options. url validates on the server through the link schema, which
accepts https://, mailto:, tel:, a leading / and a leading #, and
rejects everything else including javascript:.
textarea
Section titled “textarea”{ name: 'intro', label: 'Intro', type: 'textarea', rows: 3 }rows defaults to the browser’s default.
richtext
Section titled “richtext”{ name: 'body', label: 'Body', type: 'richtext' }Sanitised on every save, both from the form and from any AI proposal. The
toolbar’s custom styles come from editorStyles in your site config:
editorStyles: [ { className: 'block', display: 'block' }, { className: 'text-yellow-400', label: 'Brand', color: '#fbbf24' },]A style with no label is applied but not offered as a button.
{ name: 'socialImage', label: 'Sharing image', type: 'image', crop: { width: 1200, height: 630 } }With crop, the upload opens the cropper at that exact size and the stored
image is the cropped result. Without it, the file is stored as uploaded.
OG_CROP is exported as { width: 1200, height: 630 }.
avatar
Section titled “avatar”{ name: 'photo', label: 'Photo', type: 'avatar', source: 'sourcePhoto' }source names a sibling schema key holding the uncropped original, so a
face can be re-cropped later without re-uploading. Add sourcePhoto: text to
the schema and stop there. Do not add a second entry to fields: the avatar
control renders its own hidden input under that name, and a visible one beside
it is a duplicate. Leave the key out of the schema and the crop original is
never stored, so the next edit starts from the cropped image.
Avatars crop to a 512 by 512 circle.
select
Section titled “select”{ name: 'level', label: 'Level', type: 'select', options: [ { value: 'l1', label: 'Level 1' }, { value: 'l2', label: 'Level 2' }, ],}Pair with z.enum(['l1', 'l2']). The form does not enforce the option list; the
schema does.
toggle
Section titled “toggle”{ name: 'enabled', label: 'Published', type: 'toggle' }Pair with toggle or enabled, never a plain z.boolean(). An unchecked
checkbox submits the string 'false', and the helper preprocesses it.
enabledField is a ready-made definition for the common case.
stringlist
Section titled “stringlist”{ name: 'keywords', label: 'Keywords', type: 'stringlist' }A repeater of plain text rows. Stored as string[].
{ name: 'button', label: 'Call to action', type: 'group', fields: [ { name: 'label', label: 'Label', type: 'text' }, { name: 'link', label: 'Link', type: 'url' }, ],}One nested object, rendered as a fieldset. Pair with z.object, or use the cta
and linkSchema helpers.
{ name: 'people', label: 'Coaches', type: 'list', singular: 'coach', titleField: 'name', fields: [ { name: 'name', label: 'Name', type: 'text' }, { name: 'role', label: 'Role', type: 'text' }, enabledField, ],}singular fills the “Add coach” button. titleField names each row in the
collapsed header and in every history diff, so choose a field that is
actually filled in.
Rows drag to reorder. Pair with z.array(z.object({...})).
seoPreview
Section titled “seoPreview”{ name: 'socialPreview', label: 'How this looks when shared', type: 'seoPreview', show: 'social', watch: { title: 'seo.title', description: 'seo.description', shareTitle: 'openGraph.title', shareDescription: 'openGraph.description', image: 'openGraph.image', },}Stores nothing. It renders a live preview of a search result or a social card from other fields in the same form.
show is 'search', 'social' or 'both'. watch maps each slot to the
input name of the field feeding it.
Point watch at a live input wherever the value is in the same form. A server
supplied fallback for the same slot is never empty, so it shadows the input it
was meant to back up and the preview stops updating as you type. Use a server
fallback only for a value that genuinely is not on the page, such as a site
default behind a per-page field.
Adding a type
Section titled “Adding a type”See Field types.