TAW FrameworkBlocks and block registry

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:

MetaBlockBlock (UI Block)
PurposePage sections that own their dataReusable UI components
Data sourceMetaboxes → post_metaProps passed at render time
Rendered viaBlockRegistry::render('id')(new Button())->render([...])
ExamplesHero, Stats, Testimonials, CTAButton, 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
FlagDescription
--type=metaScaffold a MetaBlock
--type=uiScaffold a UI Block
--group=namePlace the block inside a subdirectory
--with-styleInclude style.scss
--with-scriptInclude script.js
--forceOverwrite 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.