This is more of a code example than a full lesson. I want to show how to register a block binding that shows the most recently sold WooCommerce product: The product image, title, price (and sale price) and rating.

To follow along, you need to have basic knowledge of how to create plugins, and how to use the block bindings API and block variations, for example by reading the introduction to the Block Bindings API.
This lesson requires WordPress 6.9 or newer.
You also need to have WooCommerce active and have at least one order of a product with a rating (You can import the sample products from WooCommerce).
Estimated reading time: 2 minutes
Last updated
First, register the block binding sources
Create a new folder inside your plugins folder and add the main plugin PHP file with the plugin header. You will only need one PHP file and one JavaScript file for this lesson.
You will not need a built process.
plugins (dir)
recently-sold (dir)
recently-sold.php
assets/js (dir)
block-variation.js<?php
/**
* Plugin Name: Recently Sold
* Version: 0.1.0
*/
Register four bindings on the init hook, one for each type of data:
function recently_sold_register_block_bindings_source() {
register_block_bindings_source(
'recently-sold/image',
array(
'label' => __( 'Product image', 'recently-sold' ),
'get_value_callback' => 'recently_sold_get_product_value',
)
);
register_block_bindings_source(
'recently-sold/title',
array(
'label' => __( 'Product title', 'recently-sold' ),
'get_value_callback' => 'recently_sold_get_product_value',
)
);
register_block_bindings_source(
'recently-sold/price',
array(
'label' => __( 'Product price', 'recently-sold' ),
'get_value_callback' => 'recently_sold_get_product_value',
)
);
register_block_bindings_source(
'recently-sold/rating',
array(
'label' => __( 'Product rating', 'recently-sold' ),
'get_value_callback' => 'recently_sold_get_product_value',
)
);
}
add_action( 'init', 'recently_sold_register_block_bindings_source' );The only reason why I’m using four bindings, is so that the placeholder in the block editor shows the unique label. If this is not important to you, you only need one register_block_bindings_source().
Note that they all use the same callback function. The callback function will rely on a helper function that fetches the product ID of the most recently sold WooCommerce product.
Create the product helper functions
Create a new function called recently_sold_get_product_id() which uses the built-in WooCommerce function wc_get_orders().
The parameters you pass to the query should ensure that you only get one purchase, which is completed, ordered by date, descending:
function recently_sold_get_product_id() {
$orders = wc_get_orders(
array(
'limit' => 1,
'status' => array( 'wc-completed' ),
'orderby' => 'date',
'order' => 'DESC',
'return' => 'objects',
)
);
}Next, add some defensive coding to check that there really is an order in the database:
function recently_sold_get_product_id() {
$orders = wc_get_orders(
array(
'limit' => 1,
'status' => array( 'wc-completed' ),
'orderby' => 'date',
'order' => 'DESC',
'return' => 'objects',
)
);
// Return 0 if no orders are found.
if ( empty( $orders ) ) {
return 0;
}
// Get the first (and only) order from results.
$items = $orders[0]->get_items();
// Return 0 if the order has no items.
if ( empty( $items ) ) {
return 0;
}
// Get the product ID from the first order item.
$product_id = (int) reset( $items )->get_product_id();
}Check that the product still exist and is published, and return the ID:
function recently_sold_get_product_id() {
$orders = wc_get_orders(
array(
'limit' => 1,
'status' => array( 'wc-completed' ),
'orderby' => 'date',
'order' => 'DESC',
'return' => 'objects',
)
);
// Return 0 if no orders are found.
if ( empty( $orders ) ) {
return 0;
}
// Get the first (and only) order from results.
$items = $orders[0]->get_items();
// Return 0 if the order has no items.
if ( empty( $items ) ) {
return 0;
}
// Get the product ID from the first order item.
$product_id = (int) reset( $items )->get_product_id();
// Return 0 if the product ID is invalid.
if ( $product_id <= 0 || 'publish' !== get_post_status( $product_id ) ) {
return 0;
}
// Return the valid product ID.
return $product_id;
}Create the block binding callback
Now let’s get back to the actual block binding callback function.
You will use a key and value pair in the block binding arguments to identify which block attribute to update:
- The
URLof an image block to display the product image: image_url - The
contentof a paragraph to display the product title and product link: product_title - The
contentof a paragraph to display the price: product_price - The
contentof a paragraph to display the rating: product_rating
💡Please note that this code is an example; to make it production ready you would also want to cache the product data etc.
function recently_sold_get_product_value( $source_args, $block_instance, $attribute_name ) {
// Make sure WooCommerce is active first.
if ( ! function_exists( 'wc_get_orders' ) ) {
return null;
}
$product_id = recently_sold_get_product_id();
if ( empty( $product_id ) ) {
return '';
}
$key = isset( $source_args['key'] ) ? $source_args['key'] : '';
// Return the product's thumbnail image URL.
if ( 'url' === $attribute_name && 'image_url' === $key ) {
$image_id = get_post_thumbnail_id( $product_id );
if ( $image_id ) {
$image_url = wp_get_attachment_image_url( $image_id, 'woocommerce_thumbnail' );
return $image_url ? $image_url : '';
}
return '';
}
if ( 'content' !== $attribute_name ) {
return null;
}
// Return the product's title and link.
if ( 'product_title' === $key ) {
$url = get_permalink( $product_id );
$title = get_the_title( $product_id );
return sprintf( '<a href="%s">%s</a>', esc_url( $url ), esc_html( $title ) );
}
$product = wc_get_product( $product_id );
if ( ! $product ) {
return '';
}
// Return the product's price HTML. This also includes the sale price.
if ( 'product_price' === $key ) {
return wp_kses_post( $product->get_price_html() );
}
// Return the WooCommerce star rating HTML, if available.
if ( 'product_rating' === $key ) {
$rating = (float) $product->get_average_rating();
$count = (int) $product->get_review_count();
if ( $rating <= 0 ) {
return '';
}
$stars_html = wc_get_star_rating_html( $rating, $count );
return wp_kses_post( $stars_html );
}
return '';
}You can read more about these functions in the WooCommerce code reference:
Create a block variation to display the binding
Now, you can create and group the blocks that will display the binding, literally. I don’t consider myself a great designer, but you will need some sort of container to display the product nicely.
You don’t strictly need to use a block variation. The reason why I recommend it is that it makes it easier for users to insert the blocks with the bindings.
In your plugin’s assets/js folder, create a new JavaScript file called block-variation.js.
Enqueue the file with wp-blocks and wp-i18n as dependencies:
add_action( 'enqueue_block_editor_assets', 'recently_sold_enqueue_editor_assets' );
function recently_sold_enqueue_editor_assets() {
wp_enqueue_script(
'recently-sold-block-variation',
plugins_url( 'assets/js/block-variation.js', __FILE__ ),
array( 'wp-blocks', 'wp-i18n' ),
'0.1.0',
true
);
}
In the JavaScript file, register a variation of the group block:
(function ( wp ) {
const { __ } = wp.i18n;
const { registerBlockVariation } = wp.blocks;
registerBlockVariation('core/group', {
name: 'recently-sold-product',
title: __('Recently sold product', 'recently-sold-product-button'),
description: __('Group displaying the most recently sold WooCommerce product with image and link.', 'recently-sold-product-button'),
scope: ['inserter'],
icon: 'cart',
attributes: {
layout: { type: 'flex', flexWrap: 'nowrap' },
metadata: {
name: __('Recently sold product', 'recently-sold-product-button'),
},
}
});
} )( window.wp );
In the next step, add the image block as an inner block:
(function ( wp ) {
const { __ } = wp.i18n;
const { registerBlockVariation } = wp.blocks;
registerBlockVariation('core/group', {
name: 'recently-sold-product',
title: __('Recently sold product', 'recently-sold-product-button'),
description: __('Group displaying the most recently sold WooCommerce product with image and link.', 'recently-sold-product-button'),
scope: ['inserter'],
icon: 'cart',
attributes: {
layout: { type: 'flex', flexWrap: 'nowrap' },
metadata: {
name: __('Recently sold product', 'recently-sold-product-button'),
},
},
innerBlocks: [
[
'core/image',
{
metadata: {
bindings: {
url: {
source: 'recently-sold/image',
args: { key: 'image_url' },
},
},
},
className: 'is-style-rounded',
sizeSlug: 'thumbnail',
width: '60px',
alt: __('Product image', 'recently-sold-product-button'),
},
],
],
});
} )( window.wp );
Of course, the size of the image and the rounded style variation is completely option, the important part is that the metadata with the binding has the correct source and arguments.
Now the nesting of the inner blocks gets more difficult to read.
I wanted to show the title and price next to each other, and the rating below them: but keeping all the text aligned with the image. That meant adding a group with a row inside. The row is the group that has the layout type set to flex:
[
'core/group',
{
layout: { type: 'default' },
style: { spacing: { margin: { top: '0', bottom: '0' } } },
},
[
[
'core/group',
{
layout: { type: 'flex', flexWrap: 'nowrap' },
style: { spacing: { margin: { top: '0', bottom: '0' } } },
},
],
],
],
The complete JavaScript
(function ( wp ) {
const { __ } = wp.i18n;
const { registerBlockVariation } = wp.blocks;
registerBlockVariation('core/group', {
name: 'recently-sold-product',
title: __('Recently sold product', 'recently-sold-product-button'),
description: __('Group displaying the most recently sold WooCommerce product with image and link.', 'recently-sold-product-button'),
scope: ['inserter'],
icon: 'cart',
attributes: {
layout: { type: 'flex', flexWrap: 'nowrap' },
metadata: {
name: __('Recently sold product', 'recently-sold-product-button'),
},
},
innerBlocks: [
[
'core/image',
{
metadata: {
bindings: {
url: {
source: 'recently-sold/image',
args: { key: 'image_url' },
},
},
},
className: 'is-style-rounded',
sizeSlug: 'thumbnail',
width: '60px',
alt: __('Product image', 'recently-sold-product-button'),
},
],
[
'core/group',
{
layout: { type: 'default' },
style: { spacing: { margin: { top: '0', bottom: '0' } } },
},
[
[
'core/group',
{
layout: { type: 'flex', flexWrap: 'nowrap' },
style: { spacing: { margin: { top: '0', bottom: '0' } } },
},
[
[
'core/paragraph',
{
metadata: {
bindings: {
content: {
source: 'recently-sold/title',
args: { key: 'product_title' },
},
},
},
content: __('Product title', 'recently-sold-product-button'),
},
],
[
'core/paragraph',
{
metadata: {
bindings: {
content: {
source: 'recently-sold/price',
args: { key: 'product_price' },
},
},
},
content: __('Product price', 'recently-sold-product-button'),
fontSize: 'small',
},
],
],
],
[
'core/paragraph',
{
metadata: {
bindings: {
content: {
source: 'recently-sold/rating',
args: { key: 'product_rating' },
},
},
},
content: __('Product rating', 'recently-sold-product-button'),
fontSize: 'small',
style: { spacing: { margin: { top: '4px' } } },
},
],
],
],
],
});
} )( window.wp );
If you have set up your WP installation with a completed order, with or without a sale or rating, you should now be able to display the most recently sold product by typing /recent:

In the editor, the variation will only show placeholders:
