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"
}
]
{
"code": "rest_forbidden",
"message": "Sorry, you are not allowed to do that.",
"status": 401
}
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/tawscript 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, withfield,value, andoperatorkeys.
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.
Single-line text input. Combine with placeholder and required. Stores a string value.
Declaration
['id' => 'heading', 'label' => 'Heading', 'type' => 'text', 'placeholder' => 'Enter heading...', 'required' => true]
Usage
$heading = Metabox::get($postId, 'heading');
echo esc_html($heading);
URL input with browser-level URL validation. Stores a string URL.
Declaration
['id' => 'cta_url', 'label' => 'CTA URL', 'type' => 'url', 'placeholder' => 'https://...']
Usage
$url = Metabox::get($postId, 'cta_url');
echo '<a href="' . esc_url($url) . '">Learn more</a>';
Numeric input for integers or floats. Supports min, max, and step options to constrain the value and control the increment size.
Declaration
['id' => 'item_count', 'label' => 'Item Count', 'type' => 'number', 'min' => 1, 'max' => 100, 'step' => 1]
Usage
$count = (int) Metabox::get($postId, 'item_count');
Multi-line plain text area. Use rows to control height and combine with placeholder and description when you expect longer content.
Declaration
['id' => 'summary', 'label' => 'Summary', 'type' => 'textarea', 'rows' => 4, 'placeholder' => 'Enter summary...']
Usage
$summary = Metabox::get($postId, 'summary');
echo esc_html($summary);
Rich text editor field. Accepts rows, media_buttons (boolean), and teeny (boolean) options to control editor size and whether media buttons or a simplified toolbar are shown.
Declaration
['id' => 'body', 'label' => 'Body', 'type' => 'wysiwyg', 'rows' => 8, 'media_buttons' => true]
Usage
$body = Metabox::get($postId, 'body');
echo wp_kses_post($body);
Dropdown select input. Provide an options map of value => label pairs; the field stores the selected option value as a string.
Declaration
['id' => 'style', 'label' => 'Style', 'type' => 'select', 'options' => ['light' => 'Light', 'dark' => 'Dark']]
Usage
$style = Metabox::get($postId, 'style'); // 'light' or 'dark'
echo '<section class="section--' . esc_attr($style) . '">';
Single checkbox input for boolean-style flags. Stores string values '1' or '0' using a hidden input that defaults to '0'.
Declaration
['id' => 'show_cta', 'label' => 'Show CTA', 'type' => 'checkbox']
Usage
$showCta = Metabox::get_bool($postId, 'show_cta');
if ($showCta) {
// render CTA
}
Color picker input. Accepts a default value (for example #ffffff) and stores the selected color string.
Declaration
['id' => 'bg_color', 'label' => 'Background', 'type' => 'color', 'default' => '#ffffff']
Usage
$bgColor = Metabox::get_color($postId, 'bg_color', '#ffffff');
echo '<section style="background-color: ' . esc_attr($bgColor) . '">';
Slider-style range input. Supports min, max, and step options, plus an optional unit string (for example %, px) for display alongside the numeric value.
Declaration
['id' => 'min_height', 'label' => 'Min Height', 'type' => 'range', 'min' => 400, 'max' => 900, 'step' => 50, 'unit' => 'px', 'default' => 600]
Usage
$minHeight = Metabox::get($postId, 'min_height') ?: '600';
echo '<section style="min-height: ' . esc_attr($minHeight) . 'px">';
Nested group of sub-fields. Use a fields array inside the group to define child fields with their own id, type, and options. Unlike a repeater, there is always exactly one row. Sub-fields are stored as separate meta keys using the group id as a prefix.
Declaration
[
'id' => 'hero_cta',
'label' => 'CTA Button',
'type' => 'group',
'fields' => [
['id' => 'text', 'label' => 'Text', 'type' => 'text', 'width' => '50'],
['id' => 'url', 'label' => 'URL', 'type' => 'url', 'width' => '50'],
],
]
Usage
// Stored as _taw_hero_cta_text and _taw_hero_cta_url
$ctaText = Metabox::get($postId, 'hero_cta_text');
$ctaUrl = Metabox::get($postId, 'hero_cta_url');
Post selector field. Supports post_type (single or multiple types), multiple (boolean to allow multiple selections), and max (integer cap on the number of selected posts).
Declaration
// Single post
['id' => 'featured_post', 'label' => 'Featured Post', 'type' => 'post_select', 'post_type' => 'post']
// Multiple posts with a cap
['id' => 'related', 'label' => 'Related Posts', 'type' => 'post_select', 'post_type' => 'post', 'multiple' => true, 'max' => 3]
Usage
$featuredId = Metabox::get_posts($postId, 'featured_post')[0] ?? null;
$relatedIds = Metabox::get_posts($postId, 'related'); // array of post IDs
WordPress media library picker. Stores an attachment ID as an integer. Retrieve as a URL with Metabox::get_image_url($postId, 'field_id', 'size') or use $this->getImageUrl() inside a MetaBlock.
Declaration
['id' => 'hero_image', 'label' => 'Hero Image', 'type' => 'image']
Usage
$imageUrl = Metabox::get_image_url($postId, 'hero_image', 'large');
// Inside a MetaBlock:
$imageUrl = $this->getImageUrl($postId, 'hero_image', 'large');
Multi-file picker with drag-to-reorder. Stores an array of attachment IDs. Accepts limit to cap the number of files that can be selected.
Declaration
['id' => 'gallery', 'label' => 'Gallery', 'type' => 'files', 'limit' => 10]
Usage
$attachmentIds = Metabox::get($postId, 'gallery');
foreach ($attachmentIds as $id) {
echo wp_get_attachment_image($id, 'medium');
}
Dynamic list of rows where each row contains the same set of sub-fields. Use a fields array inside the repeater to define the per-row schema — any field type is supported, including image, color, post_select, and nested repeater fields. Rows are drag-and-drop sortable and individually collapsible in the admin UI.
Accepts min (minimum required rows) and max (row cap, 0 = unlimited) options. Retrieve with Metabox::get_repeater($postId, 'field_id'), which returns an array of associative arrays — one per row.
Repeaters support nested repeaters: place a repeater inside the fields array of another repeater to model hierarchical data. Up to three levels of nesting are supported.
Declaration
[
'id' => 'team_members',
'label' => 'Team Members',
'type' => 'repeater',
'min' => 1,
'max' => 10,
'fields' => [
['id' => 'name', 'label' => 'Name', 'type' => 'text', 'width' => '50'],
['id' => 'role', 'label' => 'Role', 'type' => 'text', 'width' => '50'],
['id' => 'photo', 'label' => 'Photo', 'type' => 'image'],
['id' => 'bio', 'label' => 'Bio', 'type' => 'textarea'],
],
]
Usage
$members = Metabox::get_repeater($postId, 'team_members');
foreach ($members as $member) {
echo esc_html($member['name'] ?? '');
echo esc_html($member['role'] ?? '');
}
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
| Option | Default | Description |
|---|---|---|
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
| Type | Description |
|---|---|
text | Single-line text |
email | Email address — validated with is_email() |
tel | Phone number |
url | URL |
number | Numeric input |
textarea | Multi-line text; accepts rows (default 4) |
select | Dropdown; pass options as ['value' => 'Label'] |
radio | Radio group; pass options; accepts layout ('horizontal' default / 'vertical') |
checkbox | Boolean toggle; value is '1' when checked |
checkbox_group | Multiple checkboxes; pass options; accepts layout; stored as comma-separated string |
date | Native 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 value | Grid span |
|---|---|
≤ 25 | 3 / 12 columns |
≤ 33 | 4 / 12 columns |
≤ 50 | 6 / 12 columns |
≤ 67 | 8 / 12 columns |
≤ 75 | 9 / 12 columns |
> 75 or omitted | 12 / 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>'],
| Type | Description |
|---|---|
heading | Dark section banner with label and optional subtitle |
divider | Horizontal rule (<hr>) |
html | Raw 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');
Navigation menus
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
}
}
}
}
Menu API
| Method | Returns | Description |
|---|---|---|
Menu::get($location) | ?Menu | Load a menu by its registered location slug |
$menu->items() | MenuItem[] | Root-level items |
$menu->hasItems() | bool | |
$menu->name() | string | The menu name set in WordPress admin |
MenuItem API
| Method | Returns | Description |
|---|---|---|
title() | string | Menu item label |
url() | string | Destination URL |
target() | string | '_self' or '_blank' |
openInNewTab() | bool | True when target is _blank |
hasChildren() | bool | |
children() | MenuItem[] | Direct child items |
isActive() | bool | Current page matches this item |
isActiveParent() | bool | A child of this item is the current page |
isActiveAncestor() | bool | A descendant of this item is the current page |
isInActiveTrail() | bool | This 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() | string | Object type ('page', 'post', 'custom', etc.) |
objectId() | int | The underlying post/term ID |
description() | string | Item description set in WordPress menu editor |
wpPost() | WP_Post | The 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():
| Method | Endpoint | Purpose |
|---|---|---|
GET | /taw/v1/search-posts | Post search powering post_select fields. Requires edit_posts. |
POST | /taw/v1/visual-editor/save | Save Visual Editor changes. Requires edit_posts. |
GET | /taw/v1/visual-editor/fields | Load 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
Search string. Omit to return the most recent posts.
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.
Results per page. Accepts 1–50. Defaults to 10.
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.
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
MailTesterinfunctions.phpto 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
SubmissionsHandleruses 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.