TAW FrameworkTAW Core

TAW Core Tools

Overview of the developer tools TAW Core exposes for the TAW Theme, including CLI commands, runtime APIs, forms, mail, and debugging helpers.

GET /wp-json/taw/v1/search-posts?s=hero&post_type=page&per_page=5
Authorization: Cookie (requires edit_posts capability)
[
  {
    "id": 42,
    "title": "Home",
    "post_type": "page",
    "status": "publish",
    "date": "2025-01-15T10:30:00",
    "edit_url": "https://example.com/wp-admin/post.php?post=42&action=edit",
    "permalink": "https://example.com/",
    "thumbnail": "https://example.com/wp-content/uploads/hero.jpg"
  }
]

What counts as a tool in TAW Core

TAW Core tools are the developer-facing building blocks the theme uses: command-line utilities and runtime PHP APIs.

These tools fall into two groups:

  • CLI commands — Symfony Console commands exposed through the bin/taw script in your theme. Use them to generate and manage blocks and other theme artifacts.

  • Runtime APIs and helpers — PHP classes, functions, and configuration points that run inside WordPress, such as theme boot configuration, performance tuning, block registration, admin UI engines, config-driven forms, mail templates, and debugging helpers.

TAW Theme builds on top of these TAW Core primitives. When you configure the theme or call theme helpers, you are usually driving these lower-level tools under the hood.

How TAW Core boots into the theme

The theme template boots TAW Core via the TAW\Core\Theme::boot() entrypoint. You can then adjust performance behavior with Theme::performance([...]).

This boot process wires TAW Core tools into WordPress so that CLI commands, block registration, metaboxes, and other helpers are available within your theme.

Metabox

TAW Core metaboxes accept a fields configuration array, and each field defines a type that the TAW\Core\Metabox\Metabox engine knows how to render, validate, and save.

The canonical implementation lives in the TAW\Core\Metabox\Metabox class (src/Core/Metabox/Metabox.php), which switches on the field type and handles rendering and saving.

Common field options

Most field types support a shared set of options you can mix and match as needed:

  • id — required unique key used for saving and retrieving the meta value.

  • label — human-readable label shown next to the field in the metabox UI.

  • description — help text shown below the field for additional guidance.

  • required — boolean flag to mark the field as required in the UI/validation layer.

  • width — layout hint for field width within the metabox row.

  • placeholder — placeholder text for text-like inputs.

  • conditions — show/hide logic based on other field values, with field, value, and operator keys.

Use these common options alongside the type-specific options below.

Supported field types

Each field below uses a type value that maps directly to the metabox renderer.

A full metabox fields configuration looks like this:

new Metabox([
    'id'     => 'taw_hero',
    'title'  => 'Hero Section',
    'screens' => ['page'],
    'fields' => [
        // Layout with widths
        [
            'id' => 'heading',     
            'label' => 'Heading',     
            'type' => 'text',     
            'width' => '50', 
            'required' => true
        ],
        [
            'id' => 'subheading',  
            'label' => 'Subheading',  
            'type' => 'text',     
            'width' => '50'
        ],

        // Rich text
        [
            'id' => 'body',        
            'label' => 'Body',        
            'type' => 'wysiwyg', 
            'rows' => 6
        ],

        // Select with options
        [
            'id' => 'style',
            'label' => 'Style',
            'type' => 'select',
            'options' => ['light' => 'Light', 'dark' => 'Dark']
        ],

        // Toggle
        [
            'id' => 'show_cta',    
            'label' => 'Show CTA',    
            'type' => 'checkbox'
        ],

        // Color picker
        [
            'id' => 'bg_color',    
            'label' => 'Background',  
            'type' => 'color', 
            'default' => '#ffffff'
        ],

        // Range slider
        [
            'id' => 'min_height',
            'label' => 'Min Height',
            'type' => 'range',
            'min' => 400,
            'max' => 900,
            'step' => 50,
            'unit' => 'px',
            'default' => 600
        ],

        // Image
        [
            'id' => 'image',       
            'label' => 'Background Image', 
            'type' => 'image'
        ],

        // Post selector (single)
        [
            'id' => 'featured_post', 
            'label' => 'Featured Post', 
            'type' => 'post_select', 
            'post_type' => 'post'
        ],

        // Post selector (multi, with max)
        [
            'id' => 'related',
            'label' => 'Related Posts',
            'type' => 'post_select',
            'post_type' => 'post',
            'multiple' => true,
            'max' => 3
        ],
    ],
]);

Conditional fields

Fields can show or hide based on other field values. Conditions are evaluated live in the admin UI using Alpine.js, and also server-side during save.

'fields' => [
    ['id' => 'show_cta',   'label' => 'Show CTA',    'type' => 'checkbox'],
    ['id' => 'cta_text',   'label' => 'CTA Text',    'type' => 'text',
     'conditions' => [
         ['field' => 'show_cta', 'operator' => '==', 'value' => '1'],
     ]],
    ['id' => 'cta_url',    'label' => 'CTA URL',     'type' => 'url',
     'conditions' => [
         ['field' => 'show_cta', 'operator' => '==', 'value' => '1'],
     ]],
],

Supported operators: ==, !=, contains, empty, !empty

All conditions in the array use AND logic — every condition must pass for the field to show.

Tabbed fields

Group fields into tabs using the tabs key. Each tab references field IDs from the fields array.

new Metabox([
    'id'     => 'taw_hero',
    'title'  => 'Hero Section',
    'screens' => ['page'],
    'fields' => [
        ['id' => 'heading',   'label' => 'Heading',   'type' => 'text'],
        ['id' => 'image',     'label' => 'Image',     'type' => 'image'],
        ['id' => 'bg_color',  'label' => 'Background','type' => 'color'],
        ['id' => 'show_cta',  'label' => 'Show CTA',  'type' => 'checkbox'],
        ['id' => 'cta_text',  'label' => 'CTA Text',  'type' => 'text'],
    ],
    'tabs' => [
        ['id' => 'content', 'label' => 'Content', 'fields' => ['heading', 'image']],
        ['id' => 'design',  'label' => 'Design',  'fields' => ['bg_color']],
        ['id' => 'cta',     'label' => 'CTA',     'fields' => ['show_cta', 'cta_text']],
    ],
]);

Other Metabox config options

OptionDefaultDescription
screens['page']Post types, page slugs, or page template filenames to attach to — accepts an array
context'normal'Position: 'normal', 'side', 'advanced'
priority'high'Order: 'high', 'default', 'low'
prefix'_taw_'Meta key prefix applied to all field IDs
icon(none)SVG string — displayed as the metabox icon
show_on(none)callable(WP_Post): bool — return false to hide the metabox

Metabox Retrieval API

Use these static helpers inside getData() or anywhere in your templates.

use TAW\Core\Metabox\Metabox;

// Plain text / any scalar value
$heading = Metabox::get($postId, 'hero_heading');

// Checkbox → boolean (saves as '1'/'0', returns bool)
$showCta = Metabox::get_bool($postId, 'show_cta');

// Image attachment ID → URL
$imageUrl = Metabox::get_image_url($postId, 'hero_image', 'large');

// Color with fallback
$bgColor = Metabox::get_color($postId, 'bg_color', '#ffffff');

// post_select → array of post IDs (works for single and multi)
$featuredId  = Metabox::get_posts($postId, 'featured_post')[0] ?? null;
$relatedIds  = Metabox::get_posts($postId, 'related_posts');

// repeater → array of rows, each an associative array
$teamMembers = Metabox::get_repeater($postId, 'team_members');
foreach ($teamMembers as $member) {
    echo esc_html($member['name'] ?? '');
    echo esc_html($member['role'] ?? '');
}

Forms

TAW\Core\Form\Form is a config-driven frontend form builder that handles CSRF protection, honeypot spam filtering, field validation, AJAX submission (no page reload), email delivery, and automatic submission persistence.

Registration and rendering

Forms must be registered before templates load. The correct place is inside the block's boot() method, wrapped in add_action('init', ...) so translation functions are safe. Display the form in your template with Form::display().

use TAW\Core\Form\Form;

// In your MetaBlock::boot():
public static function boot(): void
{
    add_action('init', static function () {
        Form::register([
            'id'           => 'contact',
            'submit_label' => 'Send Message',
            'messages'     => ['success' => "Thanks! We'll be in touch."],
            'email' => [
                'to_self'   => ['subject' => 'New contact',      'template' => 'contact-self'],
                'to_client' => ['subject' => 'Got your message', 'template' => 'contact-client'],
            ],
            'fields' => [
                ['id' => 'name',    'label' => 'Name',    'type' => 'text',     'required' => true],
                ['id' => 'email',   'label' => 'Email',   'type' => 'email',    'required' => true],
                ['id' => 'message', 'label' => 'Message', 'type' => 'textarea', 'required' => true],
            ],
        ]);
    });
}

// In the block's index.php template:
Form::display('contact');

When both email.to_self.template and email.to_client.template are set, delivery uses Mailer + MailTemplate. Otherwise Form falls back to plain-text wp_mail().

Input field types

TypeDescription
textSingle-line text
emailEmail address — validated with is_email()
telPhone number
urlURL
numberNumeric input
textareaMulti-line text; accepts rows (default 4)
selectDropdown; pass options as ['value' => 'Label']
radioRadio group; pass options; accepts layout ('horizontal' default / 'vertical')
checkboxBoolean toggle; value is '1' when checked
checkbox_groupMultiple checkboxes; pass options; accepts layout; stored as comma-separated string
dateNative date picker; accepts min_date and max_date (ISO format YYYY-MM-DD)

Any other value (e.g. password, hidden) is passed straight through as the HTML type attribute.

Multi-column layout

Fields live inside a 12-column CSS grid. Use the width key (as a percentage) to control how many columns a field spans. On mobile all fields collapse to full width.

'fields' => [
    ['id' => 'name',    'type' => 'text',     'label' => 'Name',    'width' => 50],
    ['id' => 'company', 'type' => 'text',     'label' => 'Company', 'width' => 50],
    ['id' => 'phone',   'type' => 'tel',      'label' => 'Phone',   'width' => 33],
    ['id' => 'email',   'type' => 'email',    'label' => 'Email',   'width' => 67],
    ['id' => 'message', 'type' => 'textarea', 'label' => 'Message', 'width' => 100],
],
width valueGrid span
≤ 253 / 12 columns
≤ 334 / 12 columns
≤ 506 / 12 columns
≤ 678 / 12 columns
≤ 759 / 12 columns
> 75 or omitted12 / 12 columns (full width)

Structural field types

Structural fields are cosmetic only — they have no id, no validation, and produce no submission data.

['type' => 'heading', 'label' => '1. Personal Data', 'subtitle' => 'General identification'],
['type' => 'divider'],
['type' => 'html', 'content' => '<p class="text-sm text-gray-500">All fields marked * are required.</p>'],
TypeDescription
headingDark section banner with label and optional subtitle
dividerHorizontal rule (<hr>)
htmlRaw HTML via content key — rendered with wp_kses_post

All fields (including structural) accept width (percentage) for column placement.

Conditional fields

Fields can show or hide based on other field values. Conditions are evaluated in the browser and re-enforced on the server — hidden fields are excluded from validation and submission data regardless of client-side state.

By default all conditions use AND logic. Add 'relation' => 'any' to switch to OR:

// AND logic (default) — show 'cta_text' only when 'show_cta' is checked
['id' => 'show_cta',  'label' => 'Show CTA',  'type' => 'checkbox'],
['id' => 'cta_text',  'label' => 'CTA Text',  'type' => 'text',
 'conditions' => [
     ['field' => 'show_cta', 'operator' => '==', 'value' => '1'],
 ]],

// OR logic — show 'spouse_name' when married OR cohabiting
['id' => 'spouse_name', 'label' => 'Spouse / Partner name', 'type' => 'text',
 'conditions' => [
     'relation' => 'any',
     'rules'    => [
         ['field' => 'estado_civil', 'operator' => '==', 'value' => 'married'],
         ['field' => 'estado_civil', 'operator' => '==', 'value' => 'cohabiting'],
     ],
 ]],

Supported operators: ==, !=, >, <, >=, <=, contains

Multi-step forms

Replace the top-level fields key with steps. Each step has a title (shown in a numbered indicator) and its own fields array. All field types, widths, and conditions work identically inside steps.

Form::register([
    'id'           => 'application',
    'submit_label' => 'Submit',
    'next_label'   => 'Continue',   // optional; default "Next"
    'prev_label'   => 'Back',       // optional; default "Back"
    'messages'     => ['success' => 'Your form has been received.'],
    'steps' => [
        [
            'title'  => 'Personal Info',
            'fields' => [
                ['type' => 'heading', 'label' => '1. General Data'],
                ['id' => 'nombre',    'label' => 'Name',  'type' => 'text',  'required' => true, 'width' => 50],
                ['id' => 'email',     'label' => 'Email', 'type' => 'email', 'required' => true, 'width' => 50],
            ],
        ],
        [
            'title'  => 'Declaration',
            'fields' => [
                ['type' => 'html', 'content' => '<p>I declare that all information provided is true.</p>'],
                ['id' => 'confirm', 'label' => 'I confirm', 'type' => 'checkbox', 'required' => true],
            ],
        ],
    ],
]);

Next validates required fields in the current step before advancing. Back navigates without validation. Submit only appears on the last step. All fields from all steps are submitted together in a single AJAX request; if server validation fails, the form auto-navigates back to the step containing the first failing field.

Submission persistence

TAW\Core\Form\SubmissionsHandler is wired up automatically by Theme::boot() — no manual instantiation needed. Every successful submission is saved as a taw_submission CPT entry viewable in WP Admin → Submissions.

Configure the webhook endpoint and HMAC secret under Settings → Form Webhook in the WordPress admin. TAW Core signs outbound submission payloads with HMAC-SHA256 so your receiver can verify authenticity.

Options page

TAW\Core\OptionsPage\OptionsPage provides site-wide settings stored in wp_options, using the same field config format as metaboxes. Configure it in inc/options.php.

new OptionsPage([
    'id'         => 'taw_settings',
    'title'      => 'TAW Settings',
    'menu_title' => 'TAW Settings',
    'capability' => 'manage_options',
    'icon'       => 'dashicons-screenoptions',
    'position'   => 2,
    'fields'     => [
        ['id' => 'company_name',  'label' => 'Company Name',  'type' => 'text', 'width' => '33.33'],
        ['id' => 'company_phone', 'label' => 'Phone Number',  'type' => 'text', 'width' => '33.33'],
        ['id' => 'company_email', 'label' => 'Email Address', 'type' => 'text', 'width' => '33.33'],
        ['id' => 'footer_text',   'label' => 'Footer Text',   'type' => 'textarea'],
        ['id' => 'logo',          'label' => 'Logo',          'type' => 'image'],
    ],
    'tabs' => [
        ['label' => 'General', 'fields' => ['company_name', 'company_phone', 'company_email']],
        ['label' => 'Footer',  'fields' => ['footer_text']],
    ],
]);

Supports the same field types as Metabox. The same tabbed layout, validation, and conditions logic from metaboxes all apply.

Retrieval

use TAW\Core\OptionsPage\OptionsPage;

$phone = OptionsPage::get('company_phone');
$logo  = OptionsPage::get_image_url('logo', 'medium');

TAW\Core\Menu\Menu wraps WordPress nav menus into a typed tree, giving you full control over markup without wp_nav_menu().

use TAW\Core\Menu\Menu;

$menu = Menu::get('primary');
if ($menu && $menu->hasItems()) {
    foreach ($menu->items() as $item) {
        echo '<a href="' . esc_url($item->url()) . '"';
        if ($item->openInNewTab()) echo ' target="_blank" rel="noopener"';
        echo '>' . esc_html($item->title()) . '</a>';

        if ($item->hasChildren()) {
            foreach ($item->children() as $child) {
                // render child item
            }
        }
    }
}
MethodReturnsDescription
Menu::get($location)?MenuLoad a menu by its registered location slug
$menu->items()MenuItem[]Root-level items
$menu->hasItems()bool
$menu->name()stringThe menu name set in WordPress admin
MethodReturnsDescription
title()stringMenu item label
url()stringDestination URL
target()string'_self' or '_blank'
openInNewTab()boolTrue when target is _blank
hasChildren()bool
children()MenuItem[]Direct child items
isActive()boolCurrent page matches this item
isActiveParent()boolA child of this item is the current page
isActiveAncestor()boolA descendant of this item is the current page
isInActiveTrail()boolThis item or any ancestor/descendant is the current page
classes()string[]Custom classes only (WP auto-classes filtered out)
wpClasses()string[]All classes including WP's auto-generated ones
objectType()stringObject type ('page', 'post', 'custom', etc.)
objectId()intThe underlying post/term ID
description()stringItem description set in WordPress menu editor
wpPost()WP_PostThe raw WP menu item object

Menus (primary, footer, etc.) are registered via register_nav_menus() in functions.php. Assign menus to locations in WordPress Admin → Appearance → Menus.

REST API

TAW Core registers the following REST endpoints automatically via Theme::boot():

MethodEndpointPurpose
GET/taw/v1/search-postsPost search powering post_select fields. Requires edit_posts.
POST/taw/v1/visual-editor/saveSave Visual Editor changes. Requires edit_posts.
GET/taw/v1/visual-editor/fieldsLoad all registered fields and current values for the editor panel. Requires edit_posts.

search-posts

TAW\Core\Rest\SearchEndpoints powers the post_select metabox field.

Query parameters

query
sstring

Search string. Omit to return the most recent posts.

query
post_typestring

Post type(s) to search — comma-separated for multiple (e.g. post,page). Defaults to post. Passing page is handled correctly even though WordPress treats it as a special case internally.

query
per_pageinteger

Results per page. Accepts 150. Defaults to 10.

query
excludestring

Comma-separated post IDs to exclude from results.

Visual Editor

TAW\Core\Editor\VisualEditor provides inline admin editing on the frontend. It is opt-in — you must explicitly enable it before calling Theme::boot():

// functions.php — before Theme::boot()
use TAW\Core\Editor\VisualEditor;

VisualEditor::enable();
Theme::boot();

Once enabled, authenticated users with edit_posts capability see an Edit Visually button in the WordPress admin bar. Appending ?taw_visual_edit=1 to any URL also activates the editing shell.

What works automatically

No template changes are required for the core experience:

  • All MetaBlock sections are wrapped in a clickable container (data-taw-block-section) with hover and active outlines.
  • Clicking a section on the page opens its fields in the editor panel.
  • Typing in a panel text field updates the matching text on the page in real time (content-matching heuristic — works when the field value appears as a discrete text node).
  • The panel shows only the blocks queued for the current page via BlockRegistry::queue().

All registered metabox fields appear in the editor panel automatically. Add 'editor' => false to a field definition to exclude it from the panel.

Changes are saved via POST /wp-json/taw/v1/visual-editor/save using the same sanitization pipeline as metaboxes.

Optional template annotations

Add annotations to make inline editing precise and to enable "Edit inline on page" mode:

// Wrap a value so it's directly clickable on the page
<?= Editor::field($data['headline'], 'hero', 'headline', 'h2') ?>

// Add data attributes to an existing element (e.g. <img>)
<img <?= Editor::attrs('hero', 'hero_image') ?> src="...">

Without annotations the panel still shows and saves all fields, and live preview works via content matching. Annotations give the editor a direct DOM reference, making live updates exact.

Mail

Use TAW\Core\Mail\MailTemplate to work with HTML or MJML templates from your theme and TAW\Core\Mail\Mailer to send emails with variable replacement.

Pre-compiled HTML templates live at mails/html/{name}.html (used in production). MJML source files live at mails/{name}.mjml and are compiled at runtime via spatie/mjml-php during development. Each template supports {{variable_name}} placeholders.

A minimal example of sending a templated email:

use TAW\Core\Mail\Mailer;

$sent = (new Mailer())
    ->to('support@acme-agency.test')
    ->subject('New contact form submission')
    ->template('contact')          // → mails/html/contact.html (prod) or mails/contact.mjml (dev)
    ->setVariables([
        'name'    => 'Jane Chen',
        'email'   => 'jane@acme.com',
        'message' => 'I would like to discuss a new project.',
    ])
    ->send();

if (! $sent) {
    // Handle failed mail transport (log, retry, etc.).
}

Mailer uses the underlying wp_mail() transport. Make sure your WordPress site is configured with a working mail provider (SMTP or transactional service) so test and production emails are delivered reliably.

TAW\Core\Mail\MailTemplate compiles templates from your theme, performs {{var}} replacement, and is responsible for producing the final HTML payload that Mailer sends.

Admin entrypoints and testing

TAW Core adds admin screens to help you operate and test these modules without writing ad-hoc scripts.

  • Tools → Test Emails — register MailTester in functions.php to get a page for sending test emails using your templates and variables to verify layout and delivery in your environment:

    (new \TAW\Core\Mail\MailTester())->register();
    
  • Settings → Form Webhook — configures the webhook URL and HMAC secret that SubmissionsHandler uses when posting saved submissions to an external endpoint.

Use these screens to confirm templates render as expected and form submissions reach your downstream systems before wiring them into live flows.