Guide 05 · Developer docs

Content types and custom entities

Which built-in type fits which purpose, how categories, tags, slugs, custom URLs and SEO settings behave across them, how revisions and publishing gate what the public sees, how to model a domain the platform has no type for, and where uploaded files live.

L1–L3
Unit 05 · Custom fields

Reading custom fields

Custom fields are read with typed getters, the same in Smarty and Twig on every entity that has them, and the input type decides what comes back. Reading each kind, the empty case, and the shape in an API response.

Who this is for. Developers writing templates or integrations that use custom fields. After this page you can read a custom field of any type in either engine and through the API, and handle a field that has no value.

Typed getters, never the shorthand

A layout area can be printed with the shorthand, {$page->intro} or {{ page.intro }}. A custom field cannot: the shorthand resolves layout areas only, so {$person->jobTitle} raises "Property jobTitle does not exist" in Smarty and prints nothing in Twig, even when the person has a jobTitle custom field. Read a custom field with the getter for its category. The getters are the same on all sixteen entities and in both engines.

GetterForReturnsWith no value
getCustomFieldText(name, default)text, any editorThe stored text. A select returns its option key, a switch '1' or '0', a date or datetime its stored stringThe field's default, then the default you pass, then null
getCustomFieldBoolean(name, default)text, switch editortrue or falseThe field's default, then the default you pass, then null
getCustomFieldImage(name)imageThe imagenull
getCustomFieldFile(name)fileThe filenull
getCustomFieldValueByName(name)any; the way to read record and recordsThe stored value, whose getContent() gives getRecordModel() (record) or getRecordModels() (records)null
getCustomFields()The custom field definitions of the record's entity, for a loop over every field
getCustomFieldValues()The values stored on this record

A date custom field comes back as its stored string. A date layout area's shorthand returns a printable date object, but custom fields have no date-aware getter. The two layers of default are explained on Defining a custom field.

Text, select and switch fields

Menu elements are the usual place for them. Here element is the menu element the menu template is looping over:

Template code
{$isMegaMenu = $element->getCustomFieldBoolean('isMegaMenu', false)}
{$icon = $element->getCustomFieldText('icon')}
{$anchor = $element->getCustomFieldText('anchorLink')}
<a href="{if $anchor}#{$anchor}{else}{$element->url}{/if}"
   target="{$element->getCustomFieldText('target', '_self')}"{if $isMegaMenu} data-mega-menu="1"{/if}>
  {if $icon}<span class="icon icon-{$icon}"></span>{/if}
  {* the link text *}
</a>
{% set isMegaMenu = element.getCustomFieldBoolean('isMegaMenu', false) %}
{% set icon = element.getCustomFieldText('icon') %}
{% set anchor = element.getCustomFieldText('anchorLink') %}
<a href="{{ anchor is empty ? element.url : '#' ~ anchor }}"
   target="{{ element.getCustomFieldText('target', '_self') }}"{% if isMegaMenu %} data-mega-menu="1"{% endif %}>
  {% if icon %}<span class="icon icon-{{ icon }}"></span>{% endif %}
  {# the link text #}
</a>

Image and file fields

getCustomFieldImage() returns the image and getCustomFieldFile() the file, or null, so test the result before using it. What a template can do with an image (resizing, converting, picture tags) is in Images end to end; the custom field only hands you the image. Its stored bytes can also be printed directly, which suits an SVG icon shown inline:

Template code
{$cv = $person->getCustomFieldFile('cv')}
{if $cv}<a href="{$cv->getPath()}">Download CV</a>{/if}

{* an SVG icon, printed inline from its stored content *}
{$svg = $element->getCustomFieldImage('iconImage')}
{if $svg}{$svg->getActiveRevision()->getVirtualFile()->getContent()}{/if}
{% set cv = person.getCustomFieldFile('cv') %}
{% if cv %}<a href="{{ cv.getPath() }}">Download CV</a>{% endif %}

{# an SVG icon, printed inline from its stored content #}
{% set svg = element.getCustomFieldImage('iconImage') %}
{% if svg %}{{ svg.activeRevision.virtualFile.content }}{% endif %}

Where those bytes are kept is on Virtual files and where uploads are stored.

Record and records fields

There is no typed getter for a record or records field. Read the stored value with getCustomFieldValueByName(), test it, then take getContent()->getRecordModel() for a record field or getContent()->getRecordModels() for a records field. Here a category has a records field featuredPosts that links blog posts:

Template code
{$featured = $category->getCustomFieldValueByName('featuredPosts')}
{if $featured}
  <ul>
  {foreach $featured->getContent()->getRecordModels() as $post}
    <li><a href="{$post->url}">{$post->name}</a></li>
  {/foreach}
  </ul>
{/if}
{% set featured = category.getCustomFieldValueByName('featuredPosts') %}
{% if featured %}
  <ul>
  {% for post in featured.getContent().getRecordModels() %}
    <li><a href="{{ post.url }}">{{ post.name }}</a></li>
  {% endfor %}
  </ul>
{% endif %}

The empty case

A field with no value never raises an error. getCustomFieldText() and getCustomFieldBoolean() return the field's default, then the default you pass, and null when neither exists. getCustomFieldImage(), getCustomFieldFile() and getCustomFieldValueByName() return null, which is why every example above tests the result before calling a method on it.

Make a misspelt field name visible

The getters look a field up by name and do not check that it exists, so getCustomFieldText('jobTitel') behaves exactly like a field with no value, in both engines. While developing, pass a default that stands out, such as 'NOT SET', so a typo shows on the page.

In an API response

An API read returns a record's custom fields under customFieldContent, an object keyed by field name. Each entry is described the way a layout area's content is: id, type (the input type's name), category and editor, then the value under the keys its category uses. A field with no stored value has no entry at all: customFieldContent only lists the fields a value has been saved for.

CategoryValue keys
texttext, or active (true or false) for a switch editor
imageimageId, imagePath
filefileId, filePath
recordrecord: {id, fullPath}
recordsrecords: a list of {id, fullPath}

A person with a text field and a switch reads back like this (the ids are illustrative); a field with no stored value, such as an unset department field, has no entry at all:

JSON
{
  "customFieldContent": {
    "jobTitle": {
      "id": 5120,
      "type": "Text",
      "category": "text",
      "editor": "text",
      "text": "Head of content"
    },
    "isPartner": {
      "id": 5121,
      "type": "Switch",
      "category": "text",
      "editor": "switch",
      "active": true
    }
  }
}

Writing custom fields goes through the same API: a customFields object keyed by field name on the record's ordinary edit route, live at once because custom fields are not versioned. A site's own form that saves a custom field, such as a profile page, uses that route too. The requests, the value for each input type and the customFieldValues rows behind them are on Custom fields in the APIs guide.

Go deeper

Was this page helpful?

Your answer goes to the docs team.