Custom Fields
This page shows you how to use custom fields from ACF, MetaBox, JetEngine, and Pods in your Unblock templates.
Supported plugins
Unblock natively integrates with these custom field plugins — no configuration needed:
When one of these plugins is active, meta() automatically resolves values through that plugin's API. You don't need to change your expressions.
Basic usage
Use meta('key') on any provider that supports custom fields:
{{ post.meta('event_date') }}
{{ user.meta('company') }}
{{ term.meta('icon') }}
Nested values work with dot access:
{{ post.meta('address').city }}
{{ post.meta('address').zip }}
Check if a field exists before using it:
{{ post.has_field('subtitle') ? post.meta('subtitle') : '' }}
has_field checks that the key is stored in the WordPress meta table, whatever its value. To test that a field has a value, use post.meta('subtitle') is not empty.
meta('key') vs meta.key
meta('key') resolves a single field — one integration hook call.
meta.key resolves all meta fields for the object, triggering the integration hook for each one. With plugins like ACF active, this adds up. It only lists the keys stored in the WordPress meta table, and meta or raw_meta without a key gives them all as a map.
Prefer meta('key') when you need specific fields.
{{ post.meta('event_date') }} {# Recommended — single hook call #}
{{ post.meta.event_date }} {# Resolves all fields first #}
raw_meta('key') bypasses integrations entirely and reads the value WordPress stores. It is still escaped as text:
{{ post.raw_meta('field_name') }}
Options
Options are passed as a hash, the second argument of meta(), as in Timber.
return_format
Controls how choice fields return their value: select, checkbox, radio, button_group and true_false with ACF; select, select_advanced, radio, checkbox_list and button_group with Meta Box; select, radio and checkbox with JetEngine.
{{ post.meta('color') }} {# Stored value: "red" #}
{{ post.meta('color', {return_format: 'label'}) }} {# Display label: "Red" #}
{{ post.meta('color', {return_format: 'value'}) }} {# Stored value: "red" #}
separator
For multi-value choice fields, controls the string joining selected values. Defaults to ', '. It only applies together with return_format.
{{ post.meta('tags', {return_format: 'label', separator: ' | '}) }}
{# Output: "Design | Code | Writing" #}
format_value
ACF only. Controls whether ACF formats the value or returns the raw database value.
{{ post.meta('wysiwyg_field') }} {# Formatted HTML #}
{{ post.meta('wysiwyg_field', {format_value: false}) }} {# Raw content #}
Defaults to true. Set to false when you need the raw stored value — useful for fields where ACF's formatting gets in the way. With transform_value, it is ignored, as in Timber.
transform_value
Converts relational fields to objects you can chain, and date fields to dates, as Timber's option of the same name. Defaults to false: see Relational fields.
Relational fields
When a custom field stores a relationship to another object (post, user, term, or attachment), meta() returns what the field plugin returns, as in Timber: with ACF, the field's return format decides (an array, an ID, a URL, or a post object).
Pass {transform_value: true} to get objects you can chain instead, whatever the return format:
{{ post.meta('hero_image').url }} {# ACF image returning an array #}
{{ post.meta('hero_image', {transform_value: true}).src('large') }} {# Image object #}
To convert everywhere on the site, as Timber's timber/meta/transform_value filter does:
add_filter( 'unblock/data/meta_transform_value', '__return_true' );
{transform_value: false} still turns the conversion off for one call. Without the option, Meta Box returns rwmb_meta()'s value, Pods its own field value, and JetEngine the stored IDs.
With transform_value, date fields also become dates, as Timber does with ACF: they print with the site's date format, take the date filters, and compare with date(), as in Twig:
{% if post.meta('event_date', {transform_value: true}) >= date() %}
Supported field types
| Plugin | Field type | Resolves to |
|---|---|---|
| ACF | Post Object, Relationship | Post |
| ACF | User | User |
| ACF | Taxonomy | Term |
| ACF | Image, File, Gallery | Image / Attachment |
| ACF | Date Picker, Date Time Picker | Date |
| MetaBox | Post | Post |
| MetaBox | Image Advanced, File Advanced | Image / Attachment |
| MetaBox | Date, Datetime | Date |
| JetEngine | Posts | Post |
| JetEngine | Media, Gallery | Image / Attachment |
| JetEngine | Date, Datetime | Date |
| Pods | Relationship (post type) | Post |
| Pods | Relationship (user) | User |
| Pods | Relationship (taxonomy) | Term |
| Pods | File, Avatar | Image / Attachment |
| Pods | Date, Date / Time | Date |
These are the types transform_value converts.
Single value
When a field stores one object, transform_value makes it a full provider — chain any field it supports:
{{ post.meta('related_post', {transform_value: true}).title }}
{{ post.meta('related_post', {transform_value: true}).thumbnail.src('medium') }}
{{ post.meta('team_lead', {transform_value: true}).name }}
{{ post.meta('primary_category', {transform_value: true}).link }}
{{ post.meta('hero_image', {transform_value: true}).src('large') }}
An ACF Post Object field returning a post object chains without the option: post.meta('related_post').title.
Multiple values
Fields configured for multiple values (ACF Relationship, Gallery, MetaBox Image Advanced, etc.) return an array. Use a Loop block to iterate:
{# In a Loop block with source: post.meta('gallery', {transform_value: true}) #}
{{ item.src('medium') }}
{{ item.alt }}
You can also access a single item by index:
{{ post.meta('gallery', {transform_value: true})[0].src('large') }}
A JetEngine Posts field returns one post, or a list when it allows multiple posts. JetEngine media and gallery fields saved as URLs keep their URLs.
ACF options pages
ACF is the only integration that supports option meta. Fields registered on ACF options pages are available through site.option(), as in Timber:
{{ site.option('company_phone') }}
{{ site.option('footer_logo', {transform_value: true}).src }}
{{ site.option('social_links') }}
ACF options fields need no allowlist. Any other option name must be allowed first, see Site options.
site.option() only reads ACF options pages. MetaBox, Pods, and JetEngine fields are not available through site.option() — use post.meta(), user.meta(), or term.meta() for those plugins.
Resolution chain
When multiple custom field plugins are active, Unblock queries them in a fixed priority order:
| Priority | Plugin | Scope |
|---|---|---|
| 10 | ACF | post, user, term, option |
| 20 | MetaBox | post, user, term |
| 30 | Pods | post, user, term |
| 40 | JetEngine | post, user, term |
The first plugin that recognizes the field returns the value — the rest are skipped. If no plugin claims the field, Unblock falls back to WordPress get_metadata().
This means if the same meta key is registered in both ACF and MetaBox, ACF wins. In practice this rarely matters since most sites use a single custom field plugin.
Security
Private meta keys (starting with _) are blocked by default, to read and to sort or filter a loop on. WooCommerce's product keys shown to every visitor are open: _price, _regular_price, _sale_price, _sku, _stock, _stock_status, _wc_average_rating, _wc_review_count, the sale dates and the size, so a product loop sorts by price. To allow other private keys, use the unblock/data/allowed_meta_keys filter.
On users, session_tokens, user_activation_key and user_pass are blocked the same way.
User meta is private: it shows nothing until its key is opened in Settings > Privacy, whoever wrote the expression, except the first name, last name, nickname, bio and the visitor's own meta.
A user or post returned by a field plugin keeps only what the visitor may see: a user field in ACF's « Array » format, or a Pods relation, loses its private details, and a user or post that is private is left out.
Next steps
- Post provider — all post fields including
meta() - User provider — user fields and meta
- Common Patterns — real-world template examples