Shortcodes and blocks
A site editor needs to insert not only paragraphs and images but also a button, a list of recent books, a subscription form. There are two ways: the old and simple one — shortcodes — and the modern one — editor blocks.
A shortcode
A shortcode is a tag in square brackets that WordPress replaces with the result of a function:
// functions.php
add_shortcode('year', function (): string {
return date('Y');
});The text "© [year] My company" turns into "© 2026 My company".
Attributes
add_shortcode('button', function (array|string $atts): string {
$atts = shortcode_atts([
'url' => '#',
'text' => 'Details',
'color' => 'blue',
], $atts);
return sprintf(
'<a class="button button-%s" href="%s">%s</a>',
esc_attr($atts['color']),
esc_url($atts['url']),
esc_html($atts['text']),
);
});[button url="/books/" text="All books" color="green"]shortcode_atts() fills in defaults and drops unknown attributes.
Content between the tags
add_shortcode('note', function (array|string $atts, ?string $content = null): string {
return '<div class="note">' . do_shortcode(wp_kses_post($content ?? '')) . '</div>';
});[note]The discount is valid until the end of the month.[/note]do_shortcode() processes shortcodes nested inside.
A shortcode with a query
add_shortcode('latest_books', function (array|string $atts): string {
$atts = shortcode_atts(['count' => 3], $atts);
$books = get_posts(['post_type' => 'book', 'numberposts' => absint($atts['count'])]);
$html = '<ul class="latest-books">';
foreach ($books as $book) {
$html .= sprintf('<li><a href="%s">%s</a></li>', esc_url(get_permalink($book)), esc_html($book->post_title));
}
return $html . '</ul>';
});Blocks
In the Gutenberg editor content consists of blocks: a paragraph, an image, columns. An editor inserts a custom block with the mouse and configures it in the sidebar — no names or attributes to remember. A command generates a block skeleton:
npx @wordpress/create-block@latest book-cardIt creates a plugin with a block.json description file, an editor script in React and a build step.
A dynamic block
A block whose content depends on data — a list of recent books, say — is rendered in PHP:
// book-card.php
add_action('init', function () {
register_block_type(__DIR__ . '/build', [
'render_callback' => function (array $attributes): string {
$count = absint($attributes['count'] ?? 3);
return do_shortcode('[latest_books count="' . $count . '"]');
},
]);
});Block patterns
Often no custom block is needed: it is enough to save a combination of standard blocks as a pattern. An editor inserts it with one click and changes the texts:
add_action('init', function () {
register_block_pattern('mytheme/call-to-action', [
'title' => 'Call to action',
'categories' => ['featured'],
'content' => '<!-- wp:heading --><h2>Ready to start?</h2><!-- /wp:heading --><!-- wp:buttons --><div class="wp-block-buttons"><!-- wp:button --><div class="wp-block-button"><a class="wp-block-button__link">Contact us</a></div><!-- /wp:button --></div><!-- /wp:buttons -->',
]);
});Common mistake
A shortcode function must return a string, not print it. With echo the text appears not where the shortcode stands but at the very start of the post content. When markup is easier to write as plain HTML, wrap it in a buffer:
add_shortcode('hello', function (): string {
ob_start();
?>
<p class="hello">Hello!</p>
<?php
return ob_get_clean();
});