Concepts
Field types
Each collection field has a type from App\Enums\FieldTypeEnum. The type drives validation, normalization, EAV storage shape, and which React input renders in the item form.
Sources of truth
Enum: app/Enums/FieldTypeEnum.php. Settings validation: ValidatesCollectionFieldSettings. UI catalog: resources/js/lib/collection-field-types.ts. Item inputs: dynamic-item-fields.tsx.
Common settings
Available on (almost) every field via collections_fields.settings JSON:
| Key | Shape | Effect |
|---|---|---|
required | flag (true / 1 / 'true' / …) | Value required unless overridden by validation operators |
readonly | flag | Not editable in the item form |
hidden_in_form | flag | Hidden in form; skipped by the rule builder |
display_name | locale map (enabled content locales) | Localized label override |
note | locale map | Localized helper text |
default_value | mixed | Applied on create when empty (not for hash / files / relations / blocks / m2a in UI) |
validation_rules | [{ operator, value? }] | Extra operators: required, unique, min_length, max_length, min, max, regex, contains, not_contains, equals, not_equals |
validation_message | locale map | Localized message for custom rules |
layout_width | half | full | fill (default full) | Form column width |
layout_starts_new_row | flag | Force new row in the field layout UI |
placeholder | locale map | Where the input supports it |
conditions | see below | Dynamic hide / readonly / required |
Flags are normalized with a truthy check (boolean / 1 / '1' / 'true' / 'on'). Localized string maps are validated against the project’s enabled content locales (not hard-coded en/it) — see Project settings.
Field packs
Deterministic field bundles you can apply in one shot onto an existing collection from the Fields UI (Add field pack…) or via AI (ManageCollections apply_field_pack). Existing field names are skipped; missing ones are created through the same settings pipeline as HTTP create field.
| Pack key | Label | Fields |
|---|---|---|
seo_inline | SEO (inline) | seo_title (string, translatable), seo_description (textarea, translatable), seo_keywords (string, translatable), seo_alternate (string, translatable), seo_canonical (string), seo_robots (select), seo_noindex (boolean), seo_og_image / seo_facebook_image / seo_twitter_image (image). Prefer the SEO collection pack + M2O for Articles/Pages/Products. |
publishing | Publishing | status (select draft/published/archived), published_at (date), featured (boolean) |
contact | Contact | email, phone, address, city, postal_code, country |
social | Social | facebook_url, instagram_url, linkedin_url, twitter_url, youtube_url |
HTTP: POST /collections/{collection}/field-packs/{pack} (can-edit-collections). Source: app/Support/Collections/FieldPacks/.
How to add a field pack
- Add a
*FieldPackclass with a staticdefinition()(seeSeoInlineFieldPack). - Register it in
FieldPackRegistry::all(). - Ship docs + a Pest apply/skip test.
Collection packs
Starter collections. Prefer these for SEO entity / Articles / Pages / Products / Categories. Apply from the collections list (Create from pack…) or AI (apply_collection_pack). Missing dependencies are auto-created. Existing collection slugs are reused; missing fields/relations are added.
| Pack key | Creates | Requires | Relations |
|---|---|---|---|
seo | seo — short names (title, description, …) not seo_* | — | — |
categories | categories — name (tr), slug, description (tr) | — | — |
pages | pages — title, slug, status, published_at, body (blocks) | seo | M2O seo → seo |
articles | articles — title, slug, excerpt, body, status, published_at, cover | seo, categories | M2O category, M2O seo |
products | products — title, slug, description, price, images, status | seo, categories | M2O category, M2O seo |
HTTP: POST /collections/packs/{pack} (can-create-collections), optional body name / slug. Source: app/Support/Collections/CollectionPacks/.
How to add a collection pack
- Add a
*CollectionPackclass withdefinition()(key,label,collection,fields,requires,relations). - Register it in
CollectionPackRegistry::all(). - Ship docs + Pest (deps + relations + idempotent apply).
Model note: SEO is a collection; Articles/Pages/Products link via many_to_one. Use seo_inline only when you need denormalized seo_* fields on an existing collection.
Field conditions
Optional per-field rules evaluated against other field values on the item form (client) and for required on the server:
{
"logic": "and",
"rules": [{ "field": "kind", "operator": "equals", "value": "custom" }],
"hidden": false,
"readonly": false,
"required": true
}
Operators: equals, not_equals, empty, not_empty. Logic is AND only (no OR / nested groups yet). When rules match, any of hidden / readonly / required present in the object override the base settings for that request.
Collection form layout
Presentation metadata on collections.form_layout (JSON), not a field type. Fields stay leaves; layout groups them into optional tabs + collapsible sections:
{
"version": 1,
"tabs": [{ "id": "main", "label": { "en": "Main" } }],
"sections": [
{
"id": "details",
"tab_id": "main",
"label": { "en": "Details" },
"collapsible": true,
"collapsed": false,
"field_ids": [1, 2]
}
]
}
layout_width / layout_starts_new_row still apply inside each section. Unplaced fields render in an “Other” group. Endpoint: PUT /collections/{collection}/form-layout.
Storage helpers on the enum
| Method | Meaning |
|---|---|
isArrayStorage() | Value is stored as multiple EAV rows (position 0..n-1) or as a JSON array assembled from those rows |
isRelationType() | Points at another collection’s items |
isMultipleRelationType() | Relation that stores many related item IDs |
supportsTranslatable() | Whether the field may be marked per-locale (collections_fields.translatable) |
Translatable is not universal
hash, blocks, and all relation types (many_to_one, many_to_many, one_to_many, m2a, relation_tree, legacy relation / relation_many) do not support translatable. The admin UI hides the toggle; create/update (and the AI tool) force translatable=false for those types. For blocks, localization lives inside nested block fields instead. Text, media, selects, boolean/number/date/color/map/slider, and similar scalars still can.
Image multiple
image is not in FieldTypeEnum::isArrayStorage(), but CollectionField::usesArrayStorage() returns true when settings.allow_multiple is enabled. Treat image-with-multiple as array storage at runtime.
Array storage types
These return true from isArrayStorage():
multiselect, checkbox_group, checkbox_group_tree, tag, files, one_to_many, many_to_many, blocks, m2a, relation_many.
Relation types
These return true from isRelationType():
relation, many_to_one, one_to_many, many_to_many, relation_tree, relation_many.
Multiple relation IDs (isMultipleRelationType()): one_to_many, many_to_many, relation_many.
M2A exception
m2a uses array storage of { related_collection_id, related_item_id } blocks but is not classified as isRelationType() in the enum.
Every field type
Text and scalars
| Type | Value | Purpose | Array? | Relation? | Notable settings |
|---|---|---|---|---|---|
| String | string | Single-line text | no | no | input_type (string/text/integer/bigInteger/float/decimal/uuid), max_length, placeholder, icon_left/icon_right, trim, slugify, masked |
| Autocomplete | autocomplete | Combobox from static options | no | no | options[{value,label}], allow_other, placeholder |
| API autocomplete | api_autocomplete | Remote suggestions | no | no | url (may include {{value}}), results_path, text_path, value_path, trigger (debounce|throttle), rate, placeholder, icons |
| Number | number | Numeric input | no | no | min, max, step, placeholder |
| Boolean | boolean | Toggle | no | no | label_on, label_off (translated) |
| Textarea | textarea | Multi-line plain text | no | no | placeholder, rows (default 6), max_length |
| WYSIWYG | wysiwyg | Rich HTML (TipTap editor) | no | no | rows, max_length, placeholder; server strips scripts/unsafe tags on save |
| Markdown | markdown | Markdown source | no | no | same textarea-like options |
| Code | code | Code editor | no | no | language (default javascript), line_numbers, line_wrapping, template |
| Hash | hash | Auto fingerprint ID (sha256 of UUID) — not a password hash | no | no | masked; always nullable in rules |
| Slider | slider | Numeric slider | no | no | min (0), max (100), step, default_value, show_value |
Choice
| Type | Value | Purpose | Array? | Relation? | Notable settings |
|---|---|---|---|---|---|
| Select | select | Single choice | no | no | options, allow_none, allow_other (UI + validation) |
| Multiselect | multiselect | Multi choice dropdown | yes | no | options, allow_other |
| Checkbox group | checkbox_group | Multi checkboxes | yes | no | options, allow_other |
| Checkbox group tree | checkbox_group_tree | Nested multi select | yes | no | Tree options (+ children), value_combining (all|leaf) |
| Radio group | radio_group | Single radio | no | no | options, allow_other |
| Tag | tag | Tag list | yes | no | presets, separator (default ,), allow_other, lowercase, alphabetize |
Date, map, color
| Type | Value | Purpose | Array? | Relation? | Notable settings / storage |
|---|---|---|---|---|---|
| Date | date | Date / time / datetime | no | no | date_mode (date|time|datetime, default datetime), include_seconds. Dead use_24h removed (native inputs). |
| Map | map | GeoJSON Point or MultiPoint | no | no | Stored as GeoJSON (Point / MultiPoint, coordinates [lng, lat]); legacy { lat, lng } accepted on write and converted. Settings: geometry_mode (point|multipoint, default point), default_lat, default_lng, default_zoom. Leaflet OSM picker in admin; respects readonly. No LineString/Polygon yet. |
| Color | color | Color picker | no | no | opacity, preset_colors |
Files and media
| Type | Value | Purpose | Array? | Relation? | Notable settings |
|---|---|---|---|---|---|
| Image | image | File-manager image picker | conditional* | no | allow_multiple → list of file IDs; else single id; allowed_mime_types, crop_to_fit |
| File | file | Single file (legacy) | no | no | allowed_mime_types |
| Files | files | Multiple files | yes | no | allowed_mime_types |
* See callout above for allow_multiple.
Relations
Shared relation settings (where applicable):
related_collection_id— target collectiondisplay_field— field name used as labeldisplay_template— optional label templatefilter— option list filterallow_duplicates— whether the same related id may appear more than oncejunction_fields— many_to_many only:[{ "name": "sort", "type": "number" }]mini-schema for per-linkmeta(string|number|boolean)
| Type | Value | Purpose | Array? | Relation? | Extra |
|---|---|---|---|---|---|
| Relation | relation | Legacy many-to-one alias (hidden from type picker) | no | yes | Shared relation settings |
| Many to one | many_to_one | Link one related item | no | yes | Shared relation settings |
| One to many | one_to_many | Many related IDs | yes | yes | + layout (list|table) |
| Many to many | many_to_many | Junction objects { related_item_id, meta } (ints accepted on write) | yes | yes | Shared relation settings + optional junction_fields |
| Relation tree | relation_tree | Alias of M2O — no tree UI yet (hidden from picker; TODO) | no | yes | Shared relation settings |
| Relation many | relation_many | Legacy multi-relation (hidden from picker) | yes | yes | Shared relation settings |
| Blocks | blocks | Inline page-builder blocks | yes | no | block_types[]; value [{ id, type, data }] |
| M2A | m2a | Many-to-any builder | yes | no† | allowed_collection_ids[], allow_duplicates; value [{ related_collection_id, related_item_id }] |
† Not in isRelationType().
Normalized storage shapes
| Shape | Types |
|---|---|
| string / null | string, autocomplete, api_autocomplete, textarea, wysiwyg, markdown, code, select, radio_group, date, color |
| number / null | number, slider |
| bool / null | boolean |
| string[] | multiselect, checkbox_group, checkbox_group_tree, tag |
| GeoJSON Point / MultiPoint | map (legacy { lat, lng } still accepted on write) |
| int (file id) | file; image (single) |
| int[] (file ids) | files; image (multiple) |
| int (item id) | relation, many_to_one, relation_tree |
| int[] (item ids) | one_to_many, relation_many |
{ related_item_id, meta }[] | many_to_many (breaking vs bare ints; ints still accepted on write) |
block[] { id, type, data } | blocks |
| M2A block[] | m2a |
| string (hash) | hash |
Blocks field
blocks defines its schema inline on the field itself instead of pointing to another collection like m2a.
Settings
{
"max_blocks_depth": 3,
"block_types": [
{
"key": "rich_text",
"label": "Rich text",
"fields": [
{
"name": "title",
"type": "string",
"translatable": true,
"settings": {}
},
{
"name": "body",
"type": "wysiwyg",
"translatable": true,
"settings": {}
}
]
},
{
"key": "media",
"label": "Media",
"fields": [
{ "name": "image", "type": "image", "settings": {} },
{
"name": "caption",
"type": "string",
"translatable": true,
"settings": {}
}
]
}
]
}
max_blocks_depth is per field (default 3, absolute ceiling 5). Nested blocks fields inherit the root field’s max; deeper blocks types are stripped on save.
Value shape
[
{
"id": "11111111-1111-1111-1111-111111111111",
"type": "rich_text",
"data": {
"title": { "en": "Hello", "it": "Ciao" },
"body": { "en": "<p>Body</p>" }
}
}
]
Notes
wysiwyguses TipTap in the admin UI (toolbar: bold/italic/strike/headings/lists/blockquote/link/undo/redo). Paste from Word/HTML is accepted;WysiwygHtmlSanitizerstrips scripts and non-allowlisted tags on normalize/save.- The
blocksfield itself is not translatable; nested fields may be. - Nested file fields (
image,file,files) expand on the public API just like top-level file fields (including inside nested blocks). - Nested relations (
m2a,many_to_many,one_to_many) are JSON-embedded insideblock.data— same link shapes as top-level (m2a{ related_collection_id, related_item_id }, m2m{ related_item_id, meta }). There is no junction table for nested paths; SQL filter/query on nested m2m is out of scope. - Nested field
settings.conditionsuse the same AND / equals / empty operators as collection fields, but evaluate against sibling keys inblock.data, not top-level item fields. - Builder UX: drag-and-drop, duplicate, delete, collapse/expand, text summary in the header, move up/down. No live frontend/site preview.
blocksis distinct from top-levelm2a:blocksstores inline typed fragments; top-levelm2alinks out to items. You can also put anm2afield inside a block type.- Golden path: an Articles collection with a body
blocksfield —section→ nestedblocks→ leaf (rich_text/media); optionalrelated_modules(m2a) andrelated_articles(many_to_many) block types; conditional nested fields (e.g. showvideo_urlwhenmedia_kind === video).
Nested blocks example
{
"max_blocks_depth": 3,
"block_types": [
{
"key": "rich_text",
"label": "Rich text",
"fields": [
{
"name": "title",
"type": "string",
"translatable": true,
"settings": {}
},
{
"name": "body",
"type": "wysiwyg",
"translatable": true,
"settings": {}
}
]
},
{
"key": "section",
"label": "Section",
"fields": [
{
"name": "heading",
"type": "string",
"translatable": true,
"settings": {}
},
{
"name": "blocks",
"type": "blocks",
"translatable": false,
"settings": {
"block_types": [
{
"key": "rich_text",
"label": "Rich text",
"fields": [
{
"name": "body",
"type": "wysiwyg",
"translatable": true,
"settings": {}
}
]
},
{
"key": "media",
"label": "Media",
"fields": [{ "name": "image", "type": "image", "settings": {} }]
}
]
}
}
]
},
{
"key": "related_modules",
"label": "Related modules",
"fields": [
{
"name": "modules",
"type": "m2a",
"settings": { "allowed_collection_ids": [2] }
}
]
},
{
"key": "related_articles",
"label": "Related articles",
"fields": [
{
"name": "articles",
"type": "many_to_many",
"settings": { "related_collection_id": 1, "display_field": "title" }
}
]
}
]
}
Nested conditions example
Inside a block type, conditions reference sibling field names:
{
"name": "video_url",
"type": "string",
"settings": {
"conditions": {
"logic": "and",
"rules": [
{ "field": "media_kind", "operator": "equals", "value": "video" }
],
"required": true
}
}
}
Related pages
- Collections data model
- Collection items
- AI assistant model — tools that create/edit fields and items