You want a “Last updated” date or an author link inside a single post template. The Post Date and Post Author blocks don’t fit because you need the value inside a styled Paragraph, a Heading or a Button, next to your own design. Building a custom block for one line of text is overkill, and shortcodes don’t belong in block templates.
The Block Bindings API solves this. You register a source once in PHP. Then any Paragraph, Heading or Button can pull its text or URL from that source, and it keeps all of its normal styling controls. The snippet below adds a dpc/post-data source with four keys: modified, modified_iso, author and author_url.
The snippet
Put this in a small plugin, an mu-plugin, or your theme’s functions.php. It needs WordPress 6.5 or newer. For the editor to show the source label correctly, use 6.7 or newer.
add_action( 'init', 'dpc_register_post_data_binding' );
function dpc_register_post_data_binding(): void {
if ( ! function_exists( 'register_block_bindings_source' ) ) {
return;
}
register_block_bindings_source(
'dpc/post-data',
array(
'label' => __( 'Post data', 'dpc' ),
'get_value_callback' => 'dpc_post_data_binding_value',
'uses_context' => array( 'postId' ),
)
);
}
function dpc_post_data_binding_value( array $source_args, WP_Block $block, string $attribute_name ): ?string {
$key = isset( $source_args['key'] ) ? sanitize_key( $source_args['key'] ) : '';
$post_id = $block->context['postId'] ?? get_the_ID();
$post = $post_id ? get_post( (int) $post_id ) : null;
if ( ! $post ) {
return null;
}
// Never leak data from drafts or private posts to visitors who cannot read them.
if ( ! is_post_publicly_viewable( $post ) && ! current_user_can( 'read_post', $post->ID ) ) {
return null;
}
switch ( $key ) {
case 'modified':
return esc_html( get_the_modified_date( '', $post ) );
case 'modified_iso':
return esc_html( get_the_modified_date( 'c', $post ) );
case 'author':
return esc_html( get_the_author_meta( 'display_name', (int) $post->post_author ) );
case 'author_url':
return esc_url( get_author_posts_url( (int) $post->post_author ) );
}
/**
* Let other code add keys. Return an escaped string, or null to keep the block's own content.
*/
$value = apply_filters( 'dpc_post_data_binding_value', null, $key, $post, $attribute_name );
return is_string( $value ) ? $value : null;
}
Use it in a template
Block Bindings don’t have a full UI for custom sources yet, so add the binding in the Code editor view or directly in your template or pattern markup. The text inside the block is the fallback. It shows when the source returns null.
<!-- wp:paragraph {"metadata":{"bindings":{"content":{"source":"dpc/post-data","args":{"key":"modified"}}}}} -->
<p>Last updated</p>
<!-- /wp:paragraph -->
<!-- wp:buttons -->
<div class="wp-block-buttons"><!-- wp:button {"metadata":{"bindings":{"text":{"source":"dpc/post-data","args":{"key":"author"}},"url":{"source":"dpc/post-data","args":{"key":"author_url"}}}}} -->
<div class="wp-block-button"><a class="wp-block-button__link wp-element-button" href="#">Author</a></div>
<!-- /wp:button -->
</div>
<!-- /wp:buttons -->
How it works
register_block_bindings_source()registersdpc/post-data.uses_contextasks WordPress to passpostIdto every block that uses the source. This makes it work inside a Query Loop as well as on single templates.- When a bound block renders, core calls
dpc_post_data_binding_value()for each bound attribute. It then puts the returned value into the block’s HTML: the inner text forcontentandtext, and thehrefforurl. - Returning
nulltells core to leave the block unchanged, so your fallback text shows instead. - The visibility check stops a bound block from exposing the author or date of a private or draft post to someone who cannot read that post.
Customise it
Add your own keys without editing the function. Hook dpc_post_data_binding_value and return an escaped string:
add_filter( 'dpc_post_data_binding_value', 'dpc_post_data_word_count', 10, 4 );
function dpc_post_data_word_count( ?string $value, string $key, WP_Post $post, string $attribute_name ): ?string {
if ( 'word_count' !== $key ) {
return $value;
}
$words = str_word_count( wp_strip_all_tags( $post->post_content ) );
return esc_html( sprintf( _n( '%s word', '%s words', $words, 'dpc' ), number_format_i18n( $words ) ) );
}
Gotchas
- Only certain attributes can be bound: Paragraph
content, Headingcontent, Buttontext/url/linkTarget/rel, and Imageid/url/alt/title. Binding any other attribute does nothing. - The binding replaces the whole text of the block. To show a label like “Updated:”, put it in its own Paragraph and place both paragraphs in a Row group.
- In the editor, a block bound to a server-only source shows the source label (“Post data”) and can’t be edited. The real value only appears on the front end. For a live preview in the editor, register a matching source in JavaScript with
registerBlockBindingsSource()and agetValuescallback. - Always escape inside the callback. Core runs rich-text values through
wp_kses_post(), but it doesn’t know which values should be plain text.