Blocks and block registry
How BlockLoader discovers blocks automatically, how BlockRegistry queues and renders them, and the difference between MetaBlocks and UI Blocks.
Two types of blocks
TAW has two block base classes, each suited to a different job:
| MetaBlock | Block (UI Block) | |
|---|---|---|
| Purpose | Page sections that own their data | Reusable UI components |
| Data source | Metaboxes → post_meta | Props passed at render time |
| Rendered via | BlockRegistry::render('id') | (new Button())->render([...]) |
| Examples | Hero, Stats, Testimonials, CTA | Button, Card, Badge |
Auto-discovery
BlockLoader::loadAll() (called by Theme::boot()) scans the Blocks/ directory recursively at any nesting depth and loads every block class it finds. There is no registration step — create a folder with a matching class and it's live.
The only rule: the folder name must match the class name.
When BlockLoader loads any block, it automatically calls its boot() method if one exists. Use boot() for one-time setup logic that must run on every request — for example, registering hooks, form handlers, or other side effects the block depends on.
class Button extends Block
{
protected string $id = 'button';
public static function boot(): void
{
// Runs once during BlockLoader::loadAll()
add_filter('some_hook', [static::class, 'myCallback']);
}
}
Blocks/
├── Hero/
│ └── Hero.php ← class Hero extends MetaBlock ✓
└── ui/
└── Button/
└── Button.php ← class Button extends Block ✓
After adding a new block class, run composer dump-autoload to rebuild the PSR-4 classmap.
Creating a MetaBlock
MetaBlocks extend TAW\Core\Block\MetaBlock. They register metaboxes for content authoring and expose that content through getData().
// Blocks/Hero/Hero.php
namespace TAW\Blocks\Hero;
use TAW\Core\Block\MetaBlock;
use TAW\Core\Metabox\Metabox;
class Hero extends MetaBlock
{
protected string $id = 'hero';
protected function registerMetaboxes(): void
{
new Metabox([
'id' => 'taw_hero',
'title' => 'Hero Section',
'screens' => ['page'],
'fields' => [
['id' => 'hero_heading', 'label' => 'Heading', 'type' => 'text'],
['id' => 'hero_image', 'label' => 'Image', 'type' => 'image'],
],
]);
}
protected function getData(int|false $postId): array
{
return [
'heading' => $this->getMeta($postId, 'hero_heading'),
'image_url' => $this->getImageUrl($postId, 'hero_image', 'large'),
];
}
}
getData() receives int|false because get_the_ID() returns false on 404 pages. The convenience helpers getMeta(), getImageUrl(), and getRepeater() all return safe empty values when passed false, so your block renders gracefully even on error pages.
The template receives the getData() return value via extract() — every key becomes a local variable.
<!-- Blocks/Hero/index.php -->
<?php if (empty($heading)) return; ?>
<section class="hero">
<h1><?php echo esc_html($heading); ?></h1>
<?php if ($image_url): ?>
<img src="<?php echo esc_url($image_url); ?>" alt="">
<?php endif; ?>
</section>
Creating a UI Block
UI Blocks extend TAW\Core\Block\Block. Instead of metaboxes they define a defaultData() method. Props passed to render() are merged over these defaults, so missing props always have safe fallbacks.
// Blocks/Button/Button.php
namespace TAW\Blocks\Button;
use TAW\Core\Block\Block;
class Button extends Block
{
protected string $id = 'button';
protected function defaultData(): array
{
return [
'text' => '',
'url' => '#',
'style' => 'primary',
];
}
}
<!-- Blocks/Button/index.php -->
<a href="<?php echo esc_url($url); ?>" class="btn btn--<?php echo esc_attr($style); ?>">
<?php echo esc_html($text); ?>
</a>
Rendering blocks on a page
Queue and render (MetaBlocks)
Use BlockRegistry::queue() before get_header() to register block assets in <head>, then call BlockRegistry::render() where you want each block to appear.
<?php
// front-page.php
use TAW\Core\Block\BlockRegistry;
BlockRegistry::queue('hero', 'features', 'stats', 'testimonials', 'cta');
get_header();
?>
<?php BlockRegistry::render('hero'); ?>
<?php BlockRegistry::render('features'); ?>
<?php BlockRegistry::render('stats'); ?>
<?php BlockRegistry::render('testimonials'); ?>
<?php BlockRegistry::render('cta'); ?>
<?php get_footer(); ?>
Render for a specific post ID
// Render a block for an explicit post — useful outside The Loop
BlockRegistry::render('hero', $post_id);
Render a UI Block directly
(new \TAW\Blocks\Button\Button())->render([
'text' => 'Get Started',
'url' => '/contact',
'style' => 'secondary',
]);
Nesting blocks
UI Blocks compose naturally inside MetaBlock templates:
<!-- Blocks/Hero/index.php -->
<section class="hero">
<h1><?php echo esc_html($heading); ?></h1>
<?php (new \TAW\Blocks\Button\Button())->render([
'text' => 'Get Started',
'url' => '/contact',
]); ?>
</section>
Scaffolding blocks with the CLI
The fastest way to create a block is the make:block command:
php bin/taw make:block Hero --type=meta --with-style
php bin/taw make:block Button --type=ui --with-style --with-script
# Inside a subgroup (great for organisation)
php bin/taw make:block Hero --group=sections # → Blocks/sections/Hero/
php bin/taw make:block Badge --group=ui/cards # → Blocks/ui/cards/Badge/
composer dump-autoload
| Flag | Description |
|---|---|
--type=meta | Scaffold a MetaBlock |
--type=ui | Scaffold a UI Block |
--group=name | Place the block inside a subdirectory |
--with-style | Include style.scss |
--with-script | Include script.js |
--force | Overwrite an existing block |
Exporting and importing blocks
Blocks can be exported as portable ZIPs and imported into any TAW Theme project.
# Export
php bin/taw export:block Hero
php bin/taw export:block sections/Hero -o ./exports
# Import
php bin/taw import:block path/to/Hero.zip
php bin/taw import:block path/to/Hero.zip --group=sections --force
Block variations
MetaBlock supports registering multiple WordPress block variations from a single block class. Override the static variations() method to return an array of string suffixes — each suffix becomes a registered variation with its own asset handles. The empty string '' designates the default variation.
class Hero extends MetaBlock
{
protected string $id = 'hero';
public static function variations(): array
{
return ['', 'footer', 'landing']; // '' = default variation
}
// ...
}
Each variation shares the same metabox registration and template, but is registered as a separate block with its own enqueued assets. Asset IDs (CSS/JS handles) are automatically namespaced per variation so each can enqueue its own styles without collision.
MetaBlock convenience helpers
Inside getData() you can use these wrappers instead of calling Metabox::get* directly:
protected function getData(int|false $postId): array
{
return [
'heading' => $this->getMeta($postId, 'hero_heading'),
'image_url' => $this->getImageUrl($postId, 'hero_image', 'large'),
'show_cta' => $this->getMeta($postId, 'show_cta') === '1',
];
}
Both delegate to the same Metabox::get* methods — use whichever reads more clearly.
Block assets (style.scss, script.js) are auto-detected per block. They are only enqueued on pages that use that block. See Assets and Vite integration for details on how asset loading works.