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:

KeyShapeEffect
requiredflag (true / 1 / 'true' / …)Value required unless overridden by validation operators
readonlyflagNot editable in the item form
hidden_in_formflagHidden in form; skipped by the rule builder
display_namelocale map (enabled content locales)Localized label override
notelocale mapLocalized helper text
default_valuemixedApplied 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_messagelocale mapLocalized message for custom rules
layout_widthhalf | full | fill (default full)Form column width
layout_starts_new_rowflagForce new row in the field layout UI
placeholderlocale mapWhere the input supports it
conditionssee belowDynamic 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 keyLabelFields
seo_inlineSEO (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.
publishingPublishingstatus (select draft/published/archived), published_at (date), featured (boolean)
contactContactemail, phone, address, city, postal_code, country
socialSocialfacebook_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

  1. Add a *FieldPack class with a static definition() (see SeoInlineFieldPack).
  2. Register it in FieldPackRegistry::all().
  3. 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 keyCreatesRequiresRelations
seoseo — short names (title, description, …) not seo_*
categoriescategories — name (tr), slug, description (tr)
pagespages — title, slug, status, published_at, body (blocks)seoM2O seo → seo
articlesarticles — title, slug, excerpt, body, status, published_at, coverseo, categoriesM2O category, M2O seo
productsproducts — title, slug, description, price, images, statusseo, categoriesM2O 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

  1. Add a *CollectionPack class with definition() (key, label, collection, fields, requires, relations).
  2. Register it in CollectionPackRegistry::all().
  3. 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

MethodMeaning
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

TypeValuePurposeArray?Relation?Notable settings
StringstringSingle-line textnonoinput_type (string/text/integer/bigInteger/float/decimal/uuid), max_length, placeholder, icon_left/icon_right, trim, slugify, masked
AutocompleteautocompleteCombobox from static optionsnonooptions[{value,label}], allow_other, placeholder
API autocompleteapi_autocompleteRemote suggestionsnonourl (may include {{value}}), results_path, text_path, value_path, trigger (debounce|throttle), rate, placeholder, icons
NumbernumberNumeric inputnonomin, max, step, placeholder
BooleanbooleanTogglenonolabel_on, label_off (translated)
TextareatextareaMulti-line plain textnonoplaceholder, rows (default 6), max_length
WYSIWYGwysiwygRich HTML (TipTap editor)nonorows, max_length, placeholder; server strips scripts/unsafe tags on save
MarkdownmarkdownMarkdown sourcenonosame textarea-like options
CodecodeCode editornonolanguage (default javascript), line_numbers, line_wrapping, template
HashhashAuto fingerprint ID (sha256 of UUID) — not a password hashnonomasked; always nullable in rules
SlidersliderNumeric slidernonomin (0), max (100), step, default_value, show_value

Choice

TypeValuePurposeArray?Relation?Notable settings
SelectselectSingle choicenonooptions, allow_none, allow_other (UI + validation)
MultiselectmultiselectMulti choice dropdownyesnooptions, allow_other
Checkbox groupcheckbox_groupMulti checkboxesyesnooptions, allow_other
Checkbox group treecheckbox_group_treeNested multi selectyesnoTree options (+ children), value_combining (all|leaf)
Radio groupradio_groupSingle radiononooptions, allow_other
TagtagTag listyesnopresets, separator (default ,), allow_other, lowercase, alphabetize

Date, map, color

TypeValuePurposeArray?Relation?Notable settings / storage
DatedateDate / time / datetimenonodate_mode (date|time|datetime, default datetime), include_seconds. Dead use_24h removed (native inputs).
MapmapGeoJSON Point or MultiPointnonoStored 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.
ColorcolorColor pickernonoopacity, preset_colors

Files and media

TypeValuePurposeArray?Relation?Notable settings
ImageimageFile-manager image pickerconditional*noallow_multiple → list of file IDs; else single id; allowed_mime_types, crop_to_fit
FilefileSingle file (legacy)nonoallowed_mime_types
FilesfilesMultiple filesyesnoallowed_mime_types

* See callout above for allow_multiple.

Relations

Shared relation settings (where applicable):

  • related_collection_id — target collection
  • display_field — field name used as label
  • display_template — optional label template
  • filter — option list filter
  • allow_duplicates — whether the same related id may appear more than once
  • junction_fieldsmany_to_many only: [{ "name": "sort", "type": "number" }] mini-schema for per-link meta (string | number | boolean)
TypeValuePurposeArray?Relation?Extra
RelationrelationLegacy many-to-one alias (hidden from type picker)noyesShared relation settings
Many to onemany_to_oneLink one related itemnoyesShared relation settings
One to manyone_to_manyMany related IDsyesyes+ layout (list|table)
Many to manymany_to_manyJunction objects { related_item_id, meta } (ints accepted on write)yesyesShared relation settings + optional junction_fields
Relation treerelation_treeAlias of M2O — no tree UI yet (hidden from picker; TODO)noyesShared relation settings
Relation manyrelation_manyLegacy multi-relation (hidden from picker)yesyesShared relation settings
BlocksblocksInline page-builder blocksyesnoblock_types[]; value [{ id, type, data }]
M2Am2aMany-to-any builderyesno†allowed_collection_ids[], allow_duplicates; value [{ related_collection_id, related_item_id }]

† Not in isRelationType().

Normalized storage shapes

ShapeTypes
string / nullstring, autocomplete, api_autocomplete, textarea, wysiwyg, markdown, code, select, radio_group, date, color
number / nullnumber, slider
bool / nullboolean
string[]multiselect, checkbox_group, checkbox_group_tree, tag
GeoJSON Point / MultiPointmap (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

  • wysiwyg uses TipTap in the admin UI (toolbar: bold/italic/strike/headings/lists/blockquote/link/undo/redo). Paste from Word/HTML is accepted; WysiwygHtmlSanitizer strips scripts and non-allowlisted tags on normalize/save.
  • The blocks field 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 inside block.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.conditions use the same AND / equals / empty operators as collection fields, but evaluate against sibling keys in block.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.
  • blocks is distinct from top-level m2a: blocks stores inline typed fragments; top-level m2a links out to items. You can also put an m2a field inside a block type.
  • Golden path: an Articles collection with a body blocks field — section → nested blocks → leaf (rich_text / media); optional related_modules (m2a) and related_articles (many_to_many) block types; conditional nested fields (e.g. show video_url when media_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
    }
  }
}
Previous
Collections data model