Skip to content

Flexible Content

The Flexible_Content class is what powers the modular page builder in Terra projects. It takes config arrays (composed with ACF_Builder islands) and registers them as ACF Flexible Content field groups — with deterministic keys, automatic show_when resolution, and simplified location rules.

For a higher-level overview of how modules and heros work in templates, see the Modules and Heros guide.


  • Config-driven: Each module is a single PHP file returning an array
  • Island-composed fields: Uses ACF_Builder methods for field definitions
  • Deterministic keys: Generated with md5(), safe for environment migrations
  • Auto-resolves show_when: Converts field name references to ACF key-based conditional logic
  • Simplified locations: ['page_template' => 'page-modules.php'] instead of verbose ACF arrays
  • Duplicate detection: Dies with a clear error if two fields share the same name in a layout

Each module is a separate file in functions/project/config/flexible-modules/:

functions/project/config/flexible-modules/accordion.php
<?php
return array(
'label' => 'Accordion',
'fields' => array_merge(
ACF_Builder::spacing(),
ACF_Builder::bg_color(),
ACF_Builder::title(['name' => 'heading']),
ACF_Builder::repeater([
'name' => 'items',
'label' => 'Accordion Items',
'fields' => array_merge(
ACF_Builder::title(['name' => 'item_title']),
ACF_Builder::wysiwyg(['name' => 'item_content', 'toolbar' => 'basic']),
),
]),
),
);

Step 2: The index auto-loads all module files

Section titled “Step 2: The index auto-loads all module files”
functions/project/config/flexible-modules/index.php
<?php
$layouts = [];
foreach (glob(__DIR__ . '/*.php') as $file) {
$basename = basename($file, '.php');
if ($basename !== 'index') {
$layouts[$basename] = require $file;
}
}
return [
'title' => 'Flexible Modules',
'name' => 'modules',
'button_label' => 'Add Module',
'location' => [
['page_template' => 'page-modules.php'],
],
'layouts' => $layouts,
];

Step 3: The Core class passes it to Flexible_Content

Section titled “Step 3: The Core class passes it to Flexible_Content”
<?php
// In functions.php (inside Core::default())
new Flexible_Content($this->projectConfig['flexible_modules']);

That’s it. ACF fields are registered automatically.


The config array passed to Flexible_Content accepts:

ParameterTypeDescription
titlestringField group title shown in ACF
namestringField name used in get_field() (e.g., 'modules', 'heros')
button_labelstringLabel for the “Add Layout” button
maxintMaximum number of layouts allowed (optional)
menu_orderintPosition in the admin (optional)
locationarrayWhere to show this field group (see below)
layoutsarrayKeyed array of layout configs

The class supports a simplified location format:

<?php
// Shorthand (recommended)
'location' => [
['page_template' => 'page-modules.php'],
['post_type' => 'industry'],
],
// Full ACF format (also supported)
'location' => [
['param' => 'page_template', 'operator' => '==', 'value' => 'page-modules.php'],
],

Each layout in layouts accepts:

ParameterTypeDescription
labelstringDisplay name in the admin dropdown
fieldsarrayFields composed with ACF_Builder islands
displaystringACF display mode: 'block', 'table', or 'row'
minintMinimum instances of this layout
maxintMaximum instances of this layout

functions/project/config/flexible-modules/cta.php
<?php
return array(
'label' => 'Call to Action',
'fields' => array_merge(
ACF_Builder::spacing(),
ACF_Builder::bg_color(['palette' => 'default']),
ACF_Builder::pretitle(),
ACF_Builder::title(['name' => 'heading']),
ACF_Builder::text(['name' => 'description', 'rows' => 2]),
ACF_Builder::boolean(['name' => 'show_button']),
ACF_Builder::link([
'name' => 'button',
'show_when' => ['show_button', '==', '1'],
]),
),
);
flexible/module/cta.php
<?php
$spacing = get_spacing($module['section_spacing']);
$bg_class = get_bg_class($module['bg_color']);
?>
<section class="c--cta-a <?php echo $bg_class . ' ' . $spacing; ?>">
<?php if ($module['pretitle']): ?>
<p class="c--cta-a__pretitle"><?php echo esc_html($module['pretitle']); ?></p>
<?php endif; ?>
<h2 class="c--cta-a__title"><?php echo esc_html($module['heading']); ?></h2>
<?php if ($module['description']): ?>
<p class="c--cta-a__description"><?php echo esc_html($module['description']); ?></p>
<?php endif; ?>
<?php if ($module['show_button'] && $module['button']): ?>
<a href="<?php echo esc_url($module['button']['url']); ?>"
class="c--cta-a__button"
<?php echo get_target_link($module['button']); ?>>
<?php echo esc_html($module['button']['title']); ?>
</a>
<?php endif; ?>
</section>
flexible/module/index.php
<?php
case 'cta':
include(locate_template('flexible/module/cta.php', false, false));
break;

Heros work exactly the same way, with their own config directory and template directory:

  • Config: functions/project/config/flexible-heros/
  • Templates: flexible/hero/
  • Field name: 'heros' (used in get_field('heros'))

The only difference is they render above modules in page-modules.php.

Knowledge Check

Test your understanding of this section

Loading questions...