Add a Custom Block¶
This guide explains how to add a new server-rendered Gutenberg block to the tinnitus-blocks mu-plugin, which auto-discovers and registers blocks from subdirectories.
How auto-registration works¶
The tinnitus-blocks.php loader scans blocks/*/block.json on every init action and calls register_block_type() for each directory it finds:
function tinnitus_register_blocks(): void
{
$block_jsons = glob(__DIR__ . '/blocks/*/block.json');
if (false === $block_jsons) {
return;
}
foreach ($block_jsons as $block_json) {
register_block_type(dirname($block_json));
}
}
This means you do not need to touch the loader. Just create a new directory with a block.json file and the block is registered automatically.
Step-by-step¶
1. Create the block directory¶
Create a new directory under web/app/mu-plugins/tinnitus-blocks/blocks/ with a hyphenated name matching your block:
2. Define block.json¶
The block.json file declares the block metadata. Use API version 3, the tinnitus/ namespace, and the tinnitus-content category (registered by the loader):
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "tinnitus/my-block",
"title": "My Block",
"category": "tinnitus-content",
"description": "Brief description of what this block renders.",
"textdomain": "tinnitus",
"editorScript": "file:./edit.js",
"render": "file:./render.php",
"supports": {
"align": ["wide", "full"],
"spacing": {
"margin": true,
"padding": true
},
"html": false
}
}
Key fields:
name-- must use thetinnitus/prefix (e.g.,tinnitus/my-block)category-- usetinnitus-contentso the block appears in the project's custom category in the insertertextdomain-- alwaystinnitusrender-- points to the PHP file for server-side renderingeditorScript-- points to the JS file for the block editor placeholder
If the block should not appear in the block inserter (e.g., it is only used in templates), add "inserter": false to supports.
For blocks that accept attributes, declare them in the attributes key:
3. Create render.php¶
The render template receives three variables from WordPress: $attributes, $content, and $block. Follow the same file-level conventions as all project PHP:
<?php
/**
* My Block render template.
*
* @var array<string, mixed> $attributes Block attributes.
* @var string $content Block inner content.
* @var WP_Block $block Block instance.
*/
declare(strict_types=1);
defined('ABSPATH') || exit;
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'tinnitus-my-block',
]);
printf('<div %s>', $wrapper_attributes);
echo esc_html__('My block content here.', 'tinnitus');
echo '</div>';
Escape all output
Follow the project's late-escaping rule. Use esc_html(), esc_attr(), esc_url() immediately before output. The one exception is get_block_wrapper_attributes(), which returns pre-escaped attributes.
For reference, the existing breadcrumbs block demonstrates guarding against a missing dependency:
if (! function_exists('rank_math_the_breadcrumbs')) {
return;
}
$wrapper_attributes = get_block_wrapper_attributes([
'class' => 'tinnitus-breadcrumbs tinnitus-footer-breadcrumbs',
]);
$aria_label = esc_attr__('Breadcrumb', 'tinnitus');
printf('<nav %s aria-label="%s">', $wrapper_attributes, $aria_label);
rank_math_the_breadcrumbs();
echo '</nav>';
4. Create edit.js¶
The editor script provides a placeholder in the Site Editor. All custom JS follows the project's ES5 IIFE pattern -- no const/let, no arrow functions, no template literals:
( function () {
var el = wp.element.createElement;
var useBlockProps = wp.blockEditor.useBlockProps;
wp.blocks.registerBlockType( 'tinnitus/my-block', {
edit: function () {
var blockProps = useBlockProps( {
className: 'tinnitus-block-placeholder',
style: {
padding: '12px 16px',
background: '#f0f0f0',
border: '1px dashed #ccc',
borderRadius: '4px',
color: '#757575',
fontSize: '13px',
fontStyle: 'italic',
},
} );
return el( 'div', blockProps,
el( 'span', null, 'My Block \u2014 rendered on frontend' )
);
},
} );
} )();
The block name in registerBlockType() must exactly match the name in block.json.
5. Use the block in a template¶
Add the block to an FSE template (templates/*.html) or a pattern (patterns/*.php):
If the block has attributes:
6. Verify¶
- Start the local environment:
docker compose up -d - Visit
http://localhost:8080and verify the block renders on the frontend - Visit the Site Editor and confirm the placeholder appears
-
Run the quality checks:
Block category¶
All project blocks appear under the "Tinnitus Content" category in the block inserter. This category is registered by the loader:
function tinnitus_block_categories(array $categories): array
{
array_unshift($categories, [
'slug' => 'tinnitus-content',
'title' => __('Tinnitus Content', 'tinnitus'),
]);
return $categories;
}
If your block should appear in a different core category (e.g., text, media), change the category field in block.json. But for project-specific content blocks, prefer tinnitus-content.