Custom fields
Every field type you can declare on a content-type, page-type, collection-type or global field, with its config keys, write format and template output.
Fields are declared inside a type's tabsets.<key>.fields map. Each field has a type and an optional config object. This page catalogues every documented type: the standard types, the special types that drive the form pipeline and remote data, the validators used inside validate, and the image, date and wysiwyg config notes.
The catalogue covers the 20 documented standard types plus 6 special types (26 documented entries). For where each type may be declared, see content-types, itemset-types and the content model.
quilium-frontend / quilium-cms source.Common options
These keys are valid on every field, regardless of type.
| Key | Default | Notes |
|---|---|---|
label |
— | Required. Backoffice field label. |
info |
— | Helper text under the field. |
required |
false |
Marks the field as mandatory in the editor. |
translatable |
true |
Set false for slugs, anchors, CSS classes, hex values. See the multilingual model. |
default |
— | Initial value. Config/structural fields only (see per-type notes). |
placeholder |
— | Placeholder text in the input. |
fieldset |
— | Assigns the field to a visual fieldset group declared on the type. |
image, file — take an array of objects, [{id:'<uuid>'}], because the stored value really is an array of media objects: the server hydrates name, alt, url and versions back onto it when you read it. Every reference by id — items, smartselect with source: items, page — takes a flat array of id strings, ['<uuid>']. Either way, omitting one of these fields on an update can clear it, so resend the ids.Sending the object shape to an id reference is not rejected today: the server normalises it to flat ids on write and on read, on all three, so rows a previous client stored that way repair themselves. Write the flat shape anyway — the normalisation is a safety net for legacy data, not the contract, and every other layer (the backoffice field, the where filters, the index) is built on flat ids. It matters because of what the object shape used to do before that net existed: on items it corrupted silently, the listing showing the right label while the edit form's select came back empty and where never matched; on page a PUT resolved the objects to nothing and emptied the field on a 200. If you inherit a site where a relation holds data but the backoffice select is empty, rewrite that field once in the flat shape.
Standard types
Twenty documented standard types. Fields not exercised on the live fil-rouge site are flagged below.
| Type | Key config keys | Notes |
|---|---|---|
text |
translatable, placeholder, info |
Titles, anchors, CSS classes, URLs, FA icon names. Template output is auto-escaped. |
textarea |
— | Meta-description, excerpt, multi-line plain text. Auto-escaped, never piped through safe. |
wysiwyg |
config.limiter (int char cap); required, info, translatable |
The only documented per-field keys are label / required / info / translatable / config.limiter. Toolbar, allowed tags, formats and link target are not YAML-configurable. The styles palette is site-wide (miscellaneous.wysiwyg.styles). Renders as HTML. |
json |
— | Structured editable object. Use info: to document the shape; invalid JSON breaks render. Skill-sourced, not live-confirmed. |
embed |
— | Raw HTML / iframe / <script>. Renders unescaped. XSS vector. Skill-sourced, not live-confirmed. |
number |
— | Filterable and sortable numeric. Not for phone numbers (leading-zero loss). |
slug |
config.linkedField |
URL segment, unique, one per type. Per-locale slugs when translatable. |
checkbox |
config.checkedValue, config.uncheckedValue |
Persists strings instead of a boolean. Allowed as a collection filter. |
select |
config.options (key:Label map), config.multiple, config.mustMatch, config.inlineEditable, config.bulkEditable, default (an existing key) |
Static enum, keep to 8 or fewer. inlineEditable: true makes the value editable directly in the listing cells; bulkEditable opts into bulk edit. |
smartselect |
config.source = static | items | api (per-mode keys below) |
Dropdown sourced from a static map, a collection, or a remote API. In source: items mode it is an id reference — write a flat ['<uuid>']. Skill-sourced, not live-confirmed. |
items |
config.itemSet (req), config.fieldLabel, config.multiple, config.autocomplete, config.sort, config.where, config.firstValueIsEmpty |
Pick item(s) from a collection. Write a flat ['<uuid>'] — an id reference, not a media field. |
color |
config.options (keys = valid CSS color values, values = labels) |
Brand palette picker. |
page |
config.multiple, config.pageTypes, config.navigations |
Internal page link. Write a flat ['<uuid>']. Omitting the field on update can clear it, resend ids. |
image |
config.versions.<key>.{label, width, height, quality, resolutions}, config.multiple, config.max, config.onDelete (keep/cascade/orphan) |
At least one versions.<key> is required. Write [{id:'<media-uuid>'}]. See image versions. onDelete: media fate when the item is trashed — see ItemSet types → Retention. |
file |
config.multiple, config.max, config.onDelete (keep/cascade/orphan) |
PDF, CV, audio. Write [{id:'<media-uuid>'}], like image. onDelete: media fate when the item is trashed — see ItemSet types → Retention. Skill-sourced, not live-confirmed. |
repeat |
config.limit, config.fields (sub-fields; first must be a stable text/wysiwyg label) |
Two to ten identical structured items inline. |
choice |
config.choices.<key>.{name, fields} |
Flow of heterogeneous typed blocks. Discriminator _choice (see note). Each variant has its own fields and template. |
date |
default: now |
Single day YYYY-MM-DD. Render with | date: 'DD MMMM YYYY'. |
calendar |
RRULE value object: start, end, freq (DAILY/WEEKLY/MONTHLY/YEARLY/''), interval, byday[], count |
Recurring events. translatable defaults to false. See date and calendar. Skill-sourced, not live-confirmed. |
geo |
config.multiple |
Latitude/longitude. Template reads .lat / .lng. Skill-sourced, not live-confirmed. |
smartselect per-mode config keys:
config.source |
Keys |
|---|---|
static |
options, multiple, mustMatch, inlineEditable, bulkEditable |
items |
itemSet, fieldLabel, autocomplete, sort, where |
api |
format, src, fieldLabel, fieldValue, searchParamName, auth, headers |
_choice vs _type discriminator. Use _choice to discriminate a choice variant in templates. One skill's example shows _type, which is treated as a mistake, the dedicated pattern skill is authoritative and flags _type as wrong. Not exercised live on the fil-rouge site.Special fields
Form-pipeline and data-fetch types. Declared like any other field, usually one per tabset stage on a form-handling content-type.
| Type | Key config keys | Notes |
|---|---|---|
query |
itemSet (req), limit (literal int 1..100), page:'{{query.page}}', defaults, sort (list of single-key maps), with, dependsOn, where |
Render-time collection fetch. {{query.limit}} does not substitute, only {{query.page}} does. Pagination capped at 100, enumerate ?page=N. Heavily used on the live site. See template variables. |
validate |
config.rules.<field>: [rule…], config.redirect (array of one page-id string ["<uuid>"]) |
Form validation. Errors at content.form.<field>.errors (object {rule:msg}). Posted values at request.body.<field>. Success is a 302 PRG redirect to the target. to/subject/replyTo do not live here. |
sendmail |
config.template, config.attachments, config.smtp{host, port, secure, username, password, from}, config.unflatten |
to / subject / replyTo are per-instance content-block fields, not config — the schema refuses them outright ("A Sendmail field can't have a 'to' property."); set via the backoffice or update-content custom.<field>.{to,subject,replyTo}, interpolating {{name}} tight-braces. From is settable only through config.smtp.from. Email template reads posted fields at root {{name}}, block fields via _content.custom.<f> — form.<f> renders empty. Schema confirmed live; delivery not exercised (no mailbox on the demo sites). |
save |
config.key (target collection, req), config.attachments, config.path, config.updateField (upsert) |
Persist to a collection. default: now is not honored, use the system created_at. Live example: saveToCollection: { type: save, config: { key: abonnes } }. |
http |
config.src, config.method (POST/PUT/PATCH), config.timeout (≤60) |
Forward a submission to a webhook. Response is not exposed to templates. Skill-sourced, not live-confirmed. |
api |
config.format:'json', config.src, config.caching{enabled, validity} (validity in minutes, default 20), config.timeout (≤60), config.defaults, config.headers, config.auth{user, password} |
Render-time remote JSON fetch, exposed as the parsed object. src resolves in the same variable context as a query's where — including a sibling query field's result, reached by its bare field name ({{itinerary.coords.lat}}; the indexed and field.-prefixed forms both fail silently). dependsOn is refused on an api field. A failed fetch — or a mistyped variable — yields an empty object with the page still at 200, so guard on the branch you loop. A dead endpoint costs the full timeout on every cache miss. See Consume an external feed. |
query, validate, save and api are exercised on live demo sites. sendmail's config keys were validated against a live schema, but no message was ever sent — the demo sites have no mailbox. http remains skill-sourced.Where-clause operators
The vocabulary a query accepts under where:. It is shared by items and api config too, anywhere a where appears.
| Operator | Matches |
|---|---|
and: / or: |
logical grouping — a list of sub-clauses, one clause per item |
= < > <= >= |
comparison, numeric or date |
like: / ilike: |
wildcard on one named field, % as the placeholder; ilike is case-insensitive |
in: |
value present in an array |
intree: |
descendant of a node, for pages held in a navigation tree |
?& / ?| |
multi-value: ALL of / ANY of |
search: |
full text across the fields declared searchable: true on the type |
& prefix |
strict mode on any of the above — see below |
Empty variables are dropped, unless the clause is strict
By default a clause whose variable resolves to empty is removed from the query, and the remaining clauses run without it. A where reduced to nothing returns the entire collection. That is deliberate and often useful — an absent ?q= on a search page lists everything rather than nothing — but it is also the single most expensive default here, because the failure is silent and looks like a template bug.
Prefix the operator with & to keep the clause even when its variable is empty, so a missing value yields zero rows instead of all of them:
where:
'&=':
slug: '{{route.article.slug}}'
active: active
Use strict mode for anything a detail page depends on (slug), and for filters that must always apply (active, a published flag). Use the default mode when an absent value legitimately means "no filter".
{{…}} token, not of a broken loop. The known trap is {{current.<field>}}, which does not exist — current exposes only page and locale, and a block's own fields live under {{field.<name>}}.searchable: true
A per-field boolean on the type. It has one effect: it puts the field within reach of the search: operator. It is not required for like/ilike, which name their field directly, and it does not affect the back-office filters: list.
search is word-based: case- and accent-insensitive, several terms combined with AND, and no partial-word matching — a query on forê does not find forêt. It also reads the translated values of the locale being rendered, so on a bilingual site each language searches its own text. For substring behaviour on one named column, use ilike with % wildcards instead. All measured on the Lisière demo; see Build a blog, section 8.
Validators
Rules listed inside validate.config.rules.<field>: [rule…]. A rule is either a bare keyword or a {rule: {param}} map.
| Rule | Param | Behaviour |
|---|---|---|
required |
— | Value must be non-empty. |
email |
— | Must be a valid email format. |
recaptcha |
— | Checks g-recaptcha-response. |
equals |
value: '{{field.code}}' |
Exact match against another field. |
file-extensions |
allowed: 'jpg,png' |
Comma-separated allow-list. |
file-size |
max: 2097152 |
Maximum size in bytes. |
validation:
type: validate
config:
rules:
email: [email, required]
firstName: [] # no rule, but declared — see below
consent: [required]
rules is the allow-list of persisted fields, not just a validator. A field the form posts but rules does not name is dropped on save, with no error and no trace: the submission still redirects, the collection entry simply comes back without it. Declaring it with an empty array — firstName: [] — persists it without constraining it. Check every name="…" in the form template against this map; the mismatch is silent in both directions.Spam filtering (antispam)
Beyond the recaptcha rule, a validate field can run the submission through the platform's spam filter — an IP / email / content reputation check. It is opt-in per form, and as with the captcha there is no key to obtain and no account to open: the platform holds the credentials.
validation:
type: validate
config:
rules:
email: [email, required]
g-recaptcha-response: [recaptcha]
antispam:
email: '{{body.email}}'
message: 'Contact {{body.name}}: {{body.message}}'
email and message are interpolated from the POST ({{body.<field>}}) and they are all the filter sees: a field the template does not name is not checked. Keep the template in sync with the form — a message referencing fields that no longer exist interpolates to an empty string, and a filter that receives nothing waves everything through.
silent defaults to true: a submission flagged as spam still validates and redirects — the visitor sees a normal success — but every action behind the gate (sendmail, save, http) is skipped. Set silent: false to reject the submission openly, like any failed rule.
When the mail actually leaves
sendmail applies its own gate on top of validation, and it fails silently — the visitor lands on the thank-you page either way:
- Platform sender (no custom
smtp): the mail leaves only if arecaptcharule validated on this submission. No captcha rule, no notification. - Valid custom
smtp(host, username, password, from all set): the captcha requirement is lifted. - The spam filter said spam: nothing is sent, saved or forwarded, whatever the SMTP situation.
- Interpolated recipient (
tocontaining{{…}}): allowed only when the spam filter actually checked the submission. A dynamic recipient with no spam check is skipped — that combination is what turns a contact form into an open relay.
save and http only apply the spam gate: they run with or without a captcha.
Engine-sourced: read from the rendering engine's send path, not exercised live.
Image, date and wysiwyg config
Image versions
An image field defines one or more named crops under config.versions.<key>. At least one is required. Templates read field[0].versions.<key>.url.
| Version key | Behaviour |
|---|---|
width + height |
Crop to that exact box. |
width only / height only |
Proportional resize on that axis. |
resolutions: {key: ratio} |
Generates a srcset from the named ratios. |
image:
type: image
label: Image
config:
multiple: true # gallery
max: 12
versions:
card:
label: Card
width: 800
height: 600
cover:
label: Cover
width: 1600
height: 900
multiple: true + max: N turns the field into a gallery (live-confirmed). Write [{id:'<media-uuid>'}].
Date and calendar
type: dateis a single day,YYYY-MM-DD, withdefault: now. Render withvalue | date: 'DD MMMM YYYY'.type: calendarholds an RRULE value object (start,end,freq,interval,byday[],count) for recurring events. The template reads_occurrence.start/_occurrence.end, the single materialised occurrence.translatabledefaults to false. Skill-sourced, not live-confirmed.
Wysiwyg
A wysiwyg field is configured minimally per-field: only config.limiter (an integer character cap) beyond the common options. All visual styling comes from the site-wide miscellaneous.wysiwyg.styles palette, not per-field config. The field renders as HTML.
Related
Last updated September 6, 2026.