All skills
michtio avatar

/craft-content-modeling

@e594ffc

Craft CMS 5 content modeling — sections, entry types, fields, Matrix, relations, project config, and content architecture strategy. Covers choosing section types, designing entry types and field layouts, selecting field types, configuring Matrix and nested entries, relations and eager loading, and multi-site propagation. Triggers on: section types (single, channel, structure), entry types, field types, field layout design, field type selection, Matrix, nested entries, relatedTo, eager loading, .with()/.eagerly(), categories, tags, globals, global sets, preloadSingles, propagation, multi-site content, project config, YAML, content strategy, taxonomy, asset volumes, filesystems, image transforms, user groups, content permissions, entrify/entrification, CKEditor vs Matrix, CMS editions, multi-language, language groups, localization, translation method, field translation, content migration, field instances, Formie forms as elements vs project config, cross-environment Formie deployment, multi-site Formie translation, element index sources, elementSources, Entries index sidebar, tableAttributes, defaultSort, authoring schema from code (service layer vs project.yaml + pc/apply). Always use when planning content architecture, creating sections/fields, configuring Matrix, setting up relations, choosing field types, designing field layouts, planning multi-site propagation, or tidying the Entries element index. Do NOT trigger for PHP plugin/module development, custom field type code, front-end Twig, or buildchain.

Use this Skill: https://skilld.dev/gh/michtio/craftcms-claude-skills/craft-content-modeling

This session only. Nothing lands on disk.

referencesfield-types.md

≈3.3k tokens on demand. Your agent reads this file only when SKILL.md points to it.

Field Types Reference

All built-in field types in Craft CMS 5 with settings, Twig access, and query syntax. CKEditor (the primary rich text field) is a first-party plugin — see content-patterns.md for CKEditor content modeling patterns.

Contents

Documentation

Use WebFetch on specific field type doc pages for full setting details.

Text & Input

Plain Text

Returns string|null. Settings: UI mode (normal/enlarged), placeholder, character limit, byte limit, monospaced font, allow line breaks (textarea with configurable rows).

{{ entry.myField }}
{% if entry.myField|length > 100 %}...{% endif %}

Query: .myField('value'), .myField('PASTEL-*') (wildcards), .myField(':empty:'), .myField(':notempty:').

Email

Returns string|null. Validates email format automatically.

<a href="mailto:{{ entry.myField }}">{{ entry.myField }}</a>

Link (replaced URL field in 5.3)

Returns LinkData|null. Supports URL, Asset, Category, Email, Entry, Phone, SMS link types. Settings: allowed link types, custom label, target/rel/ARIA attributes (5.6.0+).

Serialising a LinkData value (toArray(), an Element API transformer that returns the raw value, |json_encode) yields a full array since 5.11.0: type, value, url, label, filename, link, attributes, defaultLabel, elementType, elementId, elementSiteId, elementTitle. On 5.3–5.10 the array was sparse, so API code had to call getUrl()/getLabel()/getElement() itself — keep doing that when the project must run on both.

The Url field is deprecated since 5.3.0 and is now an alias for Link.

Extensible via EVENT_REGISTER_LINK_TYPES — plugins can register custom link types (e.g., internal route links, tel with extensions).

{# Full <a> tag with all attributes #}
{{ entry.myField.link }}

{# Individual properties #}
{{ entry.myField.value }}     {# raw URL/value #}
{{ entry.myField.label }}     {# link text #}
{{ entry.myField.type }}      {# 'url', 'entry', 'asset', etc. #}
{{ entry.myField.element }}   {# related element when linking to entry/asset #}
{{ entry.myField.target }}    {# '_blank' or null #}

Number & Money

Number

Returns int|float|null. Settings: min/max, step size, decimal points, prefix/suffix.

{{ entry.myField }}
{{ entry.myField|number }}  {# formatted with locale #}

Query: .myField('>= 100'), .myField('< 50').

Money

Returns Money\Money|null. Stored as integers in minor units ($123.45 → 12345).

{{ entry.myField|money }}     {# formatted: $123.45 #}
{{ entry.myField.amount }}    {# raw: 12345 #}
{{ entry.myField.currency }}  {# Currency object #}

Use |money not |currency. Query uses natural values: .myField('>= 123.45').

Range

Returns int|float|null. Same as Number but renders as a slider in the CP. Since 5.5.0. Settings: min/max, step size, decimal points.

{{ entry.myField }}

Query: same as Number — .myField('>= 50').

Date & Time

Date/Time

Returns DateTime|null. Stored in UTC. Settings: date only or date+time, minute increment, min/max date.

{{ entry.myField|date('F j, Y') }}
{{ entry.myField|datetime('short') }}
{{ entry.myField|atom }}                {# ISO 8601 #}
{{ entry.myField|timestamp }}           {# Unix timestamp #}
<time datetime="{{ entry.myField|atom }}">{{ entry.myField|date('M j') }}</time>

Query: .myField('>= 2025-01-01'), .myField('< now'), .myField(['and', '>= 2025-01-01', '< 2025-07-01']).

Time

Returns DateTime|null. Stored as H:i string, hydrated to DateTime.

{{ entry.myField|time('short') }}

Option Fields

Single Option (Dropdown / Radio Buttons / Button Group)

All return SingleOptionFieldData|null.

{{ entry.myField.value }}   {# stored value #}
{{ entry.myField.label }}   {# display label #}
{{ entry.myField }}          {# outputs value #}

{# Comparison #}
{% if entry.myField.value == 'featured' %}

Radio Buttons support "other" option (5.5.0+). Button Group supports icon/color options (5.7.0+).

Multi Option (Checkboxes / Multi-Select)

Return MultiOptionsFieldData.

{% for option in entry.myField %}
    {{ option.label }} ({{ option.value }})
{% endfor %}

{# Check for specific value #}
{% if entry.myField.contains('vegan') %}

Toggle & Visual

Lightswitch

Returns bool. Settings: default value, ON/OFF labels.

{% if entry.myField %}Featured{% endif %}

Gotcha: Elements without an explicit value use the field default. This can unexpectedly exclude entries from other sections. Use strict matching: .myField({ value: true, strict: true }).

Color

Returns ColorData|null. Settings: predefined palette (5.6.0+), allow custom colors.

{{ entry.myField }}        {# hex: #ff0000 #}
{{ entry.myField.hex }}
{{ entry.myField.rgb }}    {# rgb(255,0,0) #}
{{ entry.myField.hsl }}
{{ entry.myField.luma }}   {# 0-1 luminance for contrast decisions #}

Icon

Returns IconData|null (craft\fields\data\IconData). Two properties: name (string, e.g., 'heart', 'arrow-right') and styles (array of available Font Awesome styles). Stored as a string in the database — the styles array is reconstructed at runtime from Craft's icon index.

Settings: includeProIcons (bool, whether to show Font Awesome Pro icons in the picker).

{# Icon name (IconData stringifies to the name) #}
{{ entry.myField }}            {# 'heart' #}
{{ entry.myField.name }}       {# 'heart' #}

{# Available styles #}
{{ entry.myField.styles|join(', ') }}   {# 'solid, regular, light' #}

{# Render as FontAwesome element #}
{{ tag('i', { class: 'fa-solid fa-' ~ entry.myField.name }) }}

{# Render as inline SVG from Craft's icon set #}
{% if entry.myField %}
    {{ svg("@appicons/#{entry.myField.name}.svg") }}
{% endif %}

Query: queryable as a string — .myField('heart'), .myField(':notempty:').

Country

Returns Country|null. Stored as two-letter code.

{{ entry.myField }}        {# 'BE' #}
{{ entry.myField.name }}   {# 'Belgium' (5.3.0+) #}

Relational Fields

All relational fields return element queries (not arrays). Each access returns a fresh query copy.

Properties common to all relation fields (Entries, Assets, Categories, Tags, Users):

  • viewMode — display mode in the CP: 'list', 'list-inline', 'thumbs', 'cards', 'cards-grid' (since 5.9.0)
  • allowSelfRelations (bool) — allow an element to relate to itself
  • localizeRelations (bool) — maintain separate relations per site
  • defaultPlacement (5.7.0) — 'beginning' or 'end' for newly added relations

Entries

Returns EntryQuery. Sources filter by section. Settings: Maintain Hierarchy, Branch Limit, Min/Max Relations, View Mode.

{% set related = entry.myField.all() %}
{% set first = entry.myField.one() %}
{% set count = entry.myField.count() %}
{% set exists = entry.myField.exists() %}

{# With additional criteria #}
{% set recent = entry.myField.orderBy('postDate DESC').limit(3).all() %}

{# Eager loading in loops #}
{% set items = entry.myField.eagerly().all() %}

Assets

Returns AssetQuery. Sources filter by volume. Settings: restrict upload location, allowed file types, min/max.

{% set image = entry.myField.one() %}
{% if image %}
    <img src="{{ image.getUrl('thumb') }}" alt="{{ image.alt }}">
{% endif %}

{# Multiple images #}
{% for image in entry.myField.all() %}
    {{ image.getImg('gallery') }}
{% endfor %}

Categories (legacy — use Entries field for new projects)

Returns CategoryQuery. Single source group. Selecting a nested category auto-selects ancestors.

Tags (legacy — use Entries field for new projects)

Returns TagQuery. Single source group. Authors create tags on-the-fly.

Users

Returns UserQuery. Sources filter by user group.

{% set author = entry.myField.one() %}
{% if author %}
    {{ author.fullName }} — {{ author.email }}
{% endif %}

Structured Data

Matrix

Returns EntryQuery of nested entries. The most powerful field type — see detailed section below.

Table

Returns array of row arrays. Column types: checkbox, color, date, dropdown, email, lightswitch, multi-line text, number, single-line text, time, URL, row heading.

Settings: staticRows (bool, 5.5.0) — predefined rows that can't be added/removed, only edited. maxRows / minRows — limit number of rows.

{% for row in entry.myField %}
    {{ row.columnHandle }}
{% endfor %}

{# Check if empty #}
{% if entry.myField|length %}

JSON

Returns array|null. Raw JSON editor in the CP. Useful for storing structured data that doesn't fit other field types (API payloads, configuration blobs, custom metadata).

{{ entry.myField|json_encode }}
{% set data = entry.myField %}
{{ data.someKey }}

Query: not queryable by content.

Addresses

Returns AddressQuery. Owned by parent element (not relational).

{% set address = entry.myField.one() %}
{{ address|address }}           {# formatted output #}
{{ address.addressLine1 }}
{{ address.locality }}
{{ address.countryCode }}

Content Block (5.8.0+)

Returns a single nested ContentBlock element (craft\elements\ContentBlock). Always exactly one per element — not repeatable. Ideal for reusable field groups (SEO metadata, banner config).

View modes: grouped (default, fields in a bordered group), pane (fields in a pane), inline (fields rendered inline with parent).

{{ entry.myContentBlock.metaTitle }}
{{ entry.myContentBlock.metaDescription }}

Cannot be nested within other Content Block fields.

Matrix Configuration

Entry Types in Matrix

Select from globally-defined entry types or create new ones. Entry types can be organized into named groups (5.8.0+). Local name/handle overrides available (5.6.0+).

Entry Type Per-Usage Overrides (5.6.0+)

When an entry type is used in a section or Matrix field, its name, handle, and description can be overridden for that specific context. The original entry type is unchanged — the override only applies in that section/field. This allows one entry type to serve different semantic roles in different contexts.

An overridden handle also changes which element partial renders: entry.render() checks _partials/entry/{overriddenHandle}.twig before _partials/entry/{originalHandle}.twig on Craft 5.10.14+. Earlier versions ignored the override and only looked up the original handle (craftcms/cms#18968). See the craft-site skill's element-partials.md.

View Modes

Mode Best For Since
Blocks Page builders, inline editing. Classic collapse/expand + drag reorder. 5.0.0
Cards Read-only overview. Double-click opens slideout editor. 5.0.0
Cards Grid Media galleries, visual content. Grid layout of cards. 5.9.0
Index Large datasets (100+ entries). Search, sort, filter, table toggle. 5.0.0

Matrix Properties

  • enableVersioning (bool, 5.7.0) — version nested entries alongside owner (blocks mode) or independently (cards/index)
  • pageSize (int) — pagination for index view mode
  • defaultTableColumns (array) — columns shown in table view
  • defaultIndexViewMode (string, 5.5.0) — default view in index mode
  • createButtonLabel (string) — customize the "New entry" button text

Matrix entries can have their own URI formats and templates per site (5.0.0) — enabling nested entries to have independent URLs.

Nesting

Matrix fields can contain entry types that themselves have Matrix fields. Use different view modes at each level to keep the UI manageable. Cards/Index modes open nested entries in slideouts.

Twig Patterns

{# Basic block rendering #}
{% for block in entry.contentBlocks.all() %}
    {% include '_blocks/' ~ block.type.handle ignore missing only %}
{% endfor %}

{# Filter by type #}
{% set images = entry.contentBlocks.type('image').all() %}

{# Eager load nested relations #}
{% set blocks = entry.contentBlocks.with(['image:photos']).all() %}

{# Check if Matrix field has content #}
{% if entry.contentBlocks.exists() %}

{# Access owner from inside a nested entry #}
{{ entry.owner.title }}

Versioning (5.7.0+)

Controlled by enableVersioning. Blocks view mode: nested entries versioned alongside owner. Cards/Index modes: independent versioning via slideout editor when enabled.

Source: SKILL.md on GitHub

No alerts16d3 checks · Risk SAFE
  • Gen Agent Trust Hub16d

    The skill provides comprehensive guidelines for content modeling in Craft CMS 5, including sections, fields, and project configuration. No malicious patterns, obfuscation, or unauthorized data exfiltration were detected. A low-severity finding is included regarding the potential for indirect prompt injection, as the skill instructs the agent to read local project configuration files which could be manipulated in a supply-chain attack to influence the agent's behavior.

  • Socket16d

    No alerts

  • Snyk16d

    Risk: LOW · No issues

Signed by skilld at e594ffc. This ties the file your Agent reads to that commit on GitHub. It does not review the instructions.

Last checked against GitHub 2 weeks ago.

Activeupdated 3 months ago

README badge

README badge for michtio/craftcms-claude-skills/craft-content-modeling