Skip to content

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.


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 functions

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/
PurposeReusable classes and helpers shared across all Terra projectsCode specific to the current site
Edit?Never. Updates come from the starter kitAlways. This is your workspace
ContainsTerra Classes, autoloader, shared helpersConfig files, Core class, project helpers

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.

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.php

Each of these file returns a PHP array. For example, a post type config:

functions/project/config/post-types_config.php
<?php
return [
[
'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.


The index.php file merges all sub-configs into a single associative array:

functions/project/config/index.php
<?php
return [
'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',
];

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:

<?php
class 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.


Subdirectories like flexible-modules/, flexible-heros/, and general-options/ use a glob() pattern in their index.php to auto-discover files:

functions/project/config/flexible-modules/index.php
<?php
$modules = [];
foreach (glob(__DIR__ . '/*.php') as $file) {
if (basename($file) === 'index.php') continue;
$modules[] = require $file;
}
return $modules;

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 KeyConfig FileTerra Class
post_typespost-types_config.phpCustom Post Type
taxonomiestaxonomy_config.phpCustom Taxonomy
post_type_fieldspost-type-fields_config.phpPost Type Fields
ajaxajax_config.phpAJAX Request
endpointendpoint_config.phpCustom API Endpoint
admin_controlleradmin-controller_config.phpAdmin Controller
admin_pagesadmin-pages_config.phpAdmin Page
wysiwyg_toolbarswysiwyg-toolbars_config.phpWYSIWYG Toolbars
custom_blockscustom-blocks_config.phpCustom Blocks
flexible_modulesflexible-modules/Flexible Content
flexible_herosflexible-heros/Flexible Content
general_optionsgeneral-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.


Add a Custom Post Type:

  1. Edit post-types_config.php — add an array
  2. Done. No other files need to change.

Add an AJAX handler:

  1. Edit ajax_config.php — add an array with action, callback, and sanitize rules
  2. Done.

Add a Flexible Module:

  1. Create functions/project/config/flexible-modules/my-module.php (returns config array)
  2. Create flexible/module/my-module.php (template)
  3. Add the case to flexible/module/index.php (switch statement)

Add an Options Page tab:

  1. Create functions/project/config/general-options/my-tab.php
  2. 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.


  1. Single source of truth — all project features are visible in one directory
  2. No hook spaghetti — you never write add_action() manually for standard features
  3. Easy onboarding — new developers can read the config files and understand the entire project
  4. Consistent structure — every Terra project has the same layout
  5. 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...