The Block Bindings API (stable in WordPress 6.5) lets you connect core block attributes — content on Paragraph, url/alt on Image, href/text on Button — directly to custom data sources registered in PHP, without writing a custom dynamic block. This is the FSE equivalent of ACF’s display functions: your data source provides the value, the core block renders it with all its existing editor controls.
Problem: Block attributes are connected to post meta, site options, or taxonomy terms via JavaScript in the block's edit component — but this creates duplicate data sources and means the block cannot read live data outside the editor without custom REST endpoints.
Solution: Use the WordPress Block Bindings API (WordPress 6.5+) — register a custom binding source with register_block_bindings_source(), define a get_value callback that reads from any server-side data source, and bind a block attribute to the source with "metadata": {"bindings": {"content": {"source": "my/source"}}} in the block's attributes. The block reads live data on every render.
The code below registers a custom binding source that reads post meta, shows the block markup with metadata.bindings, and demonstrates a compound binding that provides multiple attributes from a single data source call.
__( 'Post Meta', 'my-plugin' ),
'get_value_callback' => function ( array $source_args, WP_Block $block ): mixed {
$meta_key = sanitize_key( $source_args['key'] ?? '' );
if ( ! $meta_key ) {
return null;
}
$post_id = $block->context['postId'] ?? get_the_ID();
$value = get_post_meta( $post_id, $meta_key, true );
return $value ?: null;
},
'uses_context' => [ 'postId', 'postType' ],
] );
// Binding source for external REST data (cached)
register_block_bindings_source( 'my-plugin/stock-price', [
'label' => __( 'Stock Price', 'my-plugin' ),
'get_value_callback' => function ( array $source_args ): ?string {
$ticker = strtoupper( sanitize_text_field( $source_args['ticker'] ?? '' ) );
if ( ! $ticker ) {
return null;
}
$cache_key = 'stock_' . $ticker;
$price = get_transient( $cache_key );
if ( false === $price ) {
$response = wp_remote_get( "https://api.stocks.example.com/price/$ticker" );
if ( ! is_wp_error( $response ) ) {
$data = json_decode( wp_remote_retrieve_body( $response ), true );
$price = '$' . number_format( (float) ( $data['price'] ?? 0 ), 2 );
set_transient( $cache_key, $price, 5 * MINUTE_IN_SECONDS );
}
}
return $price ?: null;
},
] );
} );
{
"name": "core/paragraph",
"attributes": {
"content": "",
"metadata": {
"bindings": {
"content": {
"source": "my-plugin/post-meta",
"args": { "key": "_product_subtitle" }
}
}
}
}
}
{
"name": "core/image",
"attributes": {
"url": "",
"alt": "",
"metadata": {
"bindings": {
"url": { "source": "my-plugin/post-meta", "args": { "key": "_hero_image_url" } },
"alt": { "source": "my-plugin/post-meta", "args": { "key": "_hero_image_alt" } }
}
}
}
}
NOTE: Block Bindings only work on supported attributes of supported core blocks (Paragraph→content, Image→url/alt/title, Heading→content, Button→url/text/linkTarget) — you cannot bind arbitrary attributes on custom blocks using this API; for custom blocks, use render_callback with get_post_meta() directly instead.