Custom Block Bindings Source for Post Data in WordPress

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() registers dpc/post-data. uses_context asks WordPress to pass postId to 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 for content and text, and the href for url.
  • Returning null tells 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, Heading content, Button text/url/linkTarget/rel, and Image id/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 a getValues callback.
  • 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.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You’ll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

// newsletter

Three tested WordPress snippets a week

Blocks, security, performance, Tailwind, PHP and React. Copy, paste, ship. You'll also get DPlugins product updates. No spam, and you can unsubscribe anytime.

Click to Copy