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.
Key Features
Section titled “Key Features”- Config-driven: Each module is a single PHP file returning an array
- Island-composed fields: Uses
ACF_Buildermethods 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
How It Works
Section titled “How It Works”Step 1: Create a module config file
Section titled “Step 1: Create a module config file”Each module is a separate file in functions/project/config/flexible-modules/:
<?phpreturn 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”<?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.
Configuration Parameters
Section titled “Configuration Parameters”The config array passed to Flexible_Content accepts:
| Parameter | Type | Description |
|---|---|---|
title | string | Field group title shown in ACF |
name | string | Field name used in get_field() (e.g., 'modules', 'heros') |
button_label | string | Label for the “Add Layout” button |
max | int | Maximum number of layouts allowed (optional) |
menu_order | int | Position in the admin (optional) |
location | array | Where to show this field group (see below) |
layouts | array | Keyed array of layout configs |
Location Syntax
Section titled “Location Syntax”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'],],Layout Config
Section titled “Layout Config”Each layout in layouts accepts:
| Parameter | Type | Description |
|---|---|---|
label | string | Display name in the admin dropdown |
fields | array | Fields composed with ACF_Builder islands |
display | string | ACF display mode: 'block', 'table', or 'row' |
min | int | Minimum instances of this layout |
max | int | Maximum instances of this layout |
Creating a New Module (Step by Step)
Section titled “Creating a New Module (Step by Step)”1. Create the config file
Section titled “1. Create the config file”<?phpreturn 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'], ]), ),);2. Create the template file
Section titled “2. Create the template file”<?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>3. Add the case to the switch
Section titled “3. Add the case to the switch”<?phpcase '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 inget_field('heros'))
The only difference is they render above modules in page-modules.php.
Knowledge Check
Test your understanding of this section
Loading questions...