The Functions Directory
Every Terra theme organizes its backend logic inside the functions/ directory. This directory is split into two sides:
**framework/**→ shared across all projects**project**/→ specific to the site you’re building
Understanding this separation is key to working efficiently with Terra.
functions/ structure
Section titled “functions/ structure”Before diving into how config files work, let’s first map out the physical structure of the functions/ directory. This will help you navigate the codebase and understand where different types of logic belong.
Here’s a common view of the newest directory structure inside functions/:
functions/├── framework/ # Shared — do not edit per project│ ├── classes/ # Terra Classes (OOP PHP)│ │ ├── Custom_Post_Type.php│ │ ├── Custom_Taxonomy.php│ │ ├── AJAX_Request.php│ │ ├── Flexible_Content.php│ │ ├── ACF_Builder.php│ │ └── ...│ ├── index.php # Autoloader — class map│ └── helpers/ # Shared utility functions│└── project/ # Project-specific — this is where you work ├── config/ # All project configuration (see below) │ └── index.php # Master merge file ├── functions.php # Core class — reads config, boots framework └── helpers/ # Project-specific utility functionsFramework vs. Project
Section titled “Framework vs. Project”Now that you’ve seen the directory layout, it’s crucial to understand the fundamental distinction between framework and project code. This separation ensures that shared Terra functionality stays isolated from project-specific customization.
functions/framework/ | functions/project/ | |
|---|---|---|
| Purpose | Reusable classes and helpers shared across all Terra projects | Code specific to the current site |
| Edit? | Never. Updates come from the starter kit | Always. This is your workspace |
| Contains | Terra Classes, autoloader, shared helpers | Config files, Core class, project helpers |
config/ directory
Section titled “config/ directory”With the ground rules established, let’s explore where your actual work happens: the config/ directory. This is where Terra’s declarative approach truly shines.
All project features are declared as plain PHP arrays inside functions/project/config/. No scattered hooks or filters — just data.
project/config structure
Section titled “project/config structure”This is the common structure of the project/config directory:
functions/project/config/├── index.php # Master merge — loads and combines all configs├── default_config.php # Image sizes├── post-types_config.php # Custom Post Types├── taxonomy_config.php # Custom Taxonomies├── post-type-fields_config.php # ACF field groups for CPTs├── ajax_config.php # AJAX handlers├── endpoint_config.php # REST API endpoints├── admin-controller_config.php # Admin UI controls├── admin-pages_config.php # Custom admin pages├── custom-blocks_config.php # Custom Gutenberg blocks├── default-blocks_config.php # Default framework blocks├── wysiwyg-toolbars_config.php # TinyMCE toolbar presets├── flexible-modules/ # One file per flexible module│ ├── index.php # Auto-loads all module files via glob()│ ├── accordion.php│ ├── cta.php│ └── ...├── flexible-heros/ # One file per hero│ └── index.php├── general-options/ # One file per options tab│ ├── index.php│ ├── header.php│ ├── footer.php│ └── ...└── global-modules/ # Global reusable modules └── index.phpEach of these file returns a PHP array. For example, a post type config:
<?phpreturn [ [ 'slug' => 'project', 'name' => 'Projects', 'singular' => 'Project', 'icon' => 'dashicons-portfolio', 'supports' => ['title', 'thumbnail'], 'has_archive' => true, ], [ 'slug' => 'team', 'name' => 'Team Members', 'singular' => 'Team Member', 'icon' => 'dashicons-groups', ],];While individual config files define specific features, they all need to be brought together. That’s the job of the master config file.
index.php, The Master Config
Section titled “index.php, The Master Config”The index.php file merges all sub-configs into a single associative array:
<?phpreturn [ 'image_sizes' => require __DIR__ . '/default_config.php', 'post_types' => require __DIR__ . '/post-types_config.php', 'taxonomies' => require __DIR__ . '/taxonomy_config.php', 'post_type_fields' => require __DIR__ . '/post-type-fields_config.php', 'endpoint' => require __DIR__ . '/endpoint_config.php', 'ajax' => require __DIR__ . '/ajax_config.php', 'admin_controller' => require __DIR__ . '/admin-controller_config.php', 'admin_pages' => require __DIR__ . '/admin-pages_config.php', 'wysiwyg_toolbars' => require __DIR__ . '/wysiwyg-toolbars_config.php', 'default_blocks' => require __DIR__ . '/default-blocks_config.php', 'custom_blocks' => require __DIR__ . '/custom-blocks_config.php', 'flexible_modules' => require __DIR__ . '/flexible-modules/index.php', 'flexible_heros' => require __DIR__ . '/flexible-heros/index.php', 'general_options' => require __DIR__ . '/general-options/index.php', 'global_modules' => require __DIR__ . '/global-modules/index.php',];How Core Class Loads Everything
Section titled “How Core Class Loads Everything”Now that all configs are merged into one array, let’s see how they’re actually consumed. This is where the magic happens—the Core class bootstraps your entire theme using the config data.
The Core class in functions/project/functions.php reads the merged config and passes each section to the corresponding Terra Class:
<?phpclass Core { public function __construct() { $this->projectConfig = require THEME_PATH . '/functions/project/config/index.php'; }
protected function project(): void { foreach ($this->projectConfig['post_types'] as $cpt) { new Custom_Post_Type((object) $cpt); }
foreach ($this->projectConfig['taxonomies'] as $tax) { new Custom_Taxonomy((object) $tax); }
foreach ($this->projectConfig['ajax'] as $ajax) { new AJAX_Request((object) $ajax); }
// ... and so on for all other config sections }}Each Terra Class takes the config object and internally registers the WordPress hooks, filters, and actions needed. You never call add_action() or register_post_type() manually.
Some config directories behave differently—they don’t need you to manually register each file. Instead, they automatically discover and load any new files you add. This pattern keeps things especially clean for modular content like flexible layouts.
Auto-Loading Pattern
Section titled “Auto-Loading Pattern”Subdirectories like flexible-modules/, flexible-heros/, and general-options/ use a glob() pattern in their index.php to auto-discover files:
<?php$modules = [];foreach (glob(__DIR__ . '/*.php') as $file) { if (basename($file) === 'index.php') continue; $modules[] = require $file;}return $modules;Config ↔ Terra Class Reference
Section titled “Config ↔ Terra Class Reference”To help you navigate between configuration and implementation, here’s a complete mapping. If you’re editing a config file, this table shows you which Terra Class documentation to reference for available options and behavior.
Each config key maps to a Terra Class. Here’s the full reference:
| Config Key | Config File | Terra Class |
|---|---|---|
post_types | post-types_config.php | Custom Post Type |
taxonomies | taxonomy_config.php | Custom Taxonomy |
post_type_fields | post-type-fields_config.php | Post Type Fields |
ajax | ajax_config.php | AJAX Request |
endpoint | endpoint_config.php | Custom API Endpoint |
admin_controller | admin-controller_config.php | Admin Controller |
admin_pages | admin-pages_config.php | Admin Page |
wysiwyg_toolbars | wysiwyg-toolbars_config.php | WYSIWYG Toolbars |
custom_blocks | custom-blocks_config.php | Custom Blocks |
flexible_modules | flexible-modules/ | Flexible Content |
flexible_heros | flexible-heros/ | Flexible Content |
general_options | general-options/ | Options Page |
Now that you understand the structure and flow, let’s look at practical examples. These quick recipes show you exactly what steps are needed to add common features to your Terra project.
Adding a New Feature
Section titled “Adding a New Feature”Add a Custom Post Type:
- Edit
post-types_config.php— add an array - Done. No other files need to change.
Add an AJAX handler:
- Edit
ajax_config.php— add an array with action, callback, and sanitize rules - Done.
Add a Flexible Module:
- Create
functions/project/config/flexible-modules/my-module.php(returns config array) - Create
flexible/module/my-module.php(template) - Add the case to
flexible/module/index.php(switch statement)
Add an Options Page tab:
- Create
functions/project/config/general-options/my-tab.php - Done. The auto-loader picks it up.
After seeing how this config pattern works in practice, it’s worth stepping back to understand why Terra uses this approach. These benefits explain the design philosophy and why this structure helps teams work more efficiently.
Benefits
Section titled “Benefits”- Single source of truth — all project features are visible in one directory
- No hook spaghetti — you never write
add_action()manually for standard features - Easy onboarding — new developers can read the config files and understand the entire project
- Consistent structure — every Terra project has the same layout
- Safe to modify — adding a CPT, module, or AJAX handler is just adding an array
Knowledge Check
Test your understanding of this section
Loading questions...