| sidebar_label | sidebar_position |
|---|---|
12. Custom Blocks: Description Lists and Movie Runtime |
12 |
Core blocks cover most layout needs, but some HTML structures have no core equivalent. Description lists (<dl>, <dt>, <dd>) and semantic time elements (<time>) are two examples. This lesson builds custom blocks for both and revisits the single templates to use them.
- Understand a custom block's anatomy:
block.json,index.js,markup.php,style.css. - Know how to build parent/child block relationships using
parentandallowedBlocks. - Be able to create a dynamic block that renders via PHP.
- Understand how
usesContextlets a block read data from the query loop. - Know how
get_block_wrapper_attributes()handles wrapper output.
Copy the following directories from the fueled-movies theme:
blocks/dl/blocks/dl-item/blocks/dt/blocks/dd/blocks/movie-runtime/
Rebuild:
npm run buildThe theme's src/Blocks.php already auto-registers any block with a block.json in the blocks/ directory, so no additional PHP registration is needed.
The description list system is a four-block hierarchy for displaying movie and person metadata:
tenup/dl - Parent: the <dl> wrapper
└── tenup/dl-item - Child: a term+description pair
├── tenup/dt - Leaf: the <dt> term
└── tenup/dd - Leaf: the <dd> description (can contain other blocks)
The nesting rules are defined in each block's block.json:
{
"name": "tenup/dl-item",
"title": "Description List Item",
"parent": ["tenup/dl"]
}{
"name": "tenup/dt",
"title": "Description List Term",
"parent": ["tenup/dl-item"]
}parentrestricts where a block can be inserted.tenup/dl-itemcan only exist insidetenup/dl.tenup/dtandtenup/ddcan only exist insidetenup/dl-item.- The DL block uses
InnerBlocksto accept child blocks. The editor automatically filters the inserter to only show allowed children.
TODO_SUGGEST_SCREENSHOT
At 10up we build dynamic blocks: blocks that render on the server via PHP. The markup.php file is referenced as render in block.json:
{
"name": "tenup/dl",
"render": "file:./markup.php",
"editorScript": "file:./index.js",
"style": "file:./style.css"
}The PHP render template receives three variables:
<?php
/**
* @var array $attributes Block attributes.
* @var string $content Block content (inner blocks).
* @var WP_Block $block Block instance.
*/
if ( empty( trim( $content ) ) ) {
return;
}
$block_wrapper_attributes = get_block_wrapper_attributes();
?>
<dl <?php echo $block_wrapper_attributes; ?>>
<?php echo $content; ?>
</dl>$attributes: the block's saved attributes (fromblock.json)$content: the rendered HTML of inner blocks (already processed by WordPress)$block: the fullWP_Blockinstance, including context
get_block_wrapper_attributes() generates the correct wrapper attributes (classes, styles, IDs) based on block supports. Always use this instead of building class names manually.
:::tip Dynamic blocks avoid deprecation headaches: the markup isn't saved to the database, so you can change it anytime without migration scripts. This is why 10up uses dynamic blocks as the standard. :::
The editor side uses useBlockProps and useInnerBlocksProps from @wordpress/block-editor:
import { registerBlockType } from '@wordpress/blocks';
import { InnerBlocks } from '@wordpress/block-editor';
import { BlockEdit } from './edit';
import metadata from './block.json';
registerBlockType(metadata, {
edit: BlockEdit,
save: () => <InnerBlocks.Content />,
});For dynamic blocks, the save function returns <InnerBlocks.Content /> (if the block has inner blocks) or null (if it doesn't). The actual frontend markup comes from markup.php.
You don't need to manually register blocks. src/Blocks.php globs dist/blocks/*/block.json and calls register_block_type_from_metadata() for each:
public function register_theme_blocks() {
$block_json_files = glob( TENUP_BLOCK_THEME_BLOCK_DIST_DIR . '*/block.json' );
foreach ( $block_json_files as $filename ) {
$block_folder = dirname( $filename );
register_block_type_from_metadata( $block_folder );
}
}Drop a folder with a block.json into blocks/ and it's automatically available.
The tenup/movie-runtime block demonstrates reading data from the post via block context:
{
"name": "tenup/movie-runtime",
"usesContext": ["postId", "postType"],
"render": "file:./markup.php"
}$post_id = $block->context['postId'] ?? null;
if ( ! $post_id ) {
return;
}
$runtime = get_post_meta( $post_id, 'tenup_movie_runtime', true );
$hours = $runtime['hours'] ?? '0';
$minutes = $runtime['minutes'] ?? '0';
if ( '0' === $hours && '0' === $minutes ) {
return;
}
?>
<time <?php echo get_block_wrapper_attributes( [
'datetime' => esc_attr( 'PT' . $hours . 'H' . $minutes . 'M' ),
] ); ?>>
<?php // renders "2h 28m" with ARIA labels ?>
</time>The usesContext declaration in block.json tells WordPress to pass postId and postType from the query loop context. This lets the block read meta for whatever post it's rendering inside, without hardcoding a post ID.
Update both single templates in the Site Editor to use the new blocks:
Single Movie (templates/single-tenup-movie.html):
- Wrap plot, stars, and genre in
tenup/dlblocks - Add
tenup/movie-runtimeto the metadata row
<!-- wp:tenup/dl {"style":{"layout":{"selfStretch":"fill"}},"layout":{"type":"default"}} -->
<!-- wp:tenup/dl-item {"layout":{"type":"flex","flexWrap":"nowrap","verticalAlignment":"top"}} -->
<!-- wp:tenup/dt {"content":"Genre","style":{"layout":{"selfStretch":"fixed","flexSize":"5.5rem"}},"textColor":"text-secondary"} /-->
<!-- wp:tenup/dd {"style":{"layout":{"selfStretch":"fill"}}} -->
<!-- wp:post-terms {"term":"tenup-genre"} /-->
<!-- /wp:tenup/dd -->
<!-- /wp:tenup/dl-item -->
<!-- wp:tenup/dl-item {"layout":{"type":"flex","flexWrap":"nowrap","verticalAlignment":"top"}} -->
<!-- wp:tenup/dt {"content":"Plot","textColor":"text-secondary"} /-->
<!-- wp:tenup/dd -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"tenup_movie_plot"}}}}} -->
<p></p>
<!-- /wp:paragraph -->
<!-- /wp:tenup/dd -->
<!-- /wp:tenup/dl-item -->
<!-- wp:tenup/dl-item {"layout":{"type":"flex","flexWrap":"nowrap","verticalAlignment":"top"}} -->
<!-- wp:tenup/dt {"content":"Stars","textColor":"text-secondary"} /-->
<!-- wp:tenup/dd -->
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"tenup/block-bindings","args":{"key":"movieStars"}}}}} -->
<p></p>
<!-- /wp:paragraph -->
<!-- /wp:tenup/dd -->
<!-- /wp:tenup/dl-item -->
<!-- /wp:tenup/dl -->Single Person (templates/single-tenup-person.html):
- Wrap biography, born, birthplace, died, deathplace, and movies in
tenup/dlblocks
Export the updated markup back to the theme files.
TODO_SUGGEST_SCREENSHOT
| File | Change type | What changes |
|---|---|---|
blocks/dl/ |
New | Block metadata, edit component, markup.php, styles |
blocks/dl-item/ |
New | parent: ["tenup/dl"], markup.php |
blocks/dt/ |
New | parent: ["tenup/dl-item"], inline editable term |
blocks/dd/ |
New | parent: ["tenup/dl-item"], inner blocks container |
blocks/movie-runtime/ |
New | usesContext: ["postId", "postType"], semantic <time> output |
templates/single-tenup-movie.html |
Revisited | Plot/Stars/Genre wrapped in tenup/dl blocks; tenup/movie-runtime added to metadata row |
templates/single-tenup-person.html |
Revisited | All metadata paragraphs wrapped in tenup/dl blocks |
- DL blocks enforce nesting: only dl-item inside dl, only dt/dd inside dl-item
- Movie runtime displays as "2h 28m" with semantic
<time>element - Single movie template has DL with Genre, Plot, Stars
- Single person template has DL with Biography, Born, Birthplace, Died, Deathplace, Movies
- Custom blocks:
block.jsonfor metadata,index.jsfor the editor,markup.phpfor the frontend. - Dynamic blocks (PHP-rendered) avoid deprecation problems: the 10up standard.
- Use
parentandallowedBlocksto enforce nesting rules in parent/child block systems. get_block_wrapper_attributes()handles wrapper classes, styles, and IDs. Always use it.usesContextinblock.jsonlets blocks read data from query loop context.- Drop a folder with
block.jsonintoblocks/and auto-registration handles the rest.