WordPress.org

WordPress Developer Blog

Building a book review site with Block Bindings, part 2: Queries, patterns, and templates

Building a book review site with Block Bindings, part 2: Queries, patterns, and templates

It’s been a little over a month since you and I built a book review site using custom fields together. In Part 1 of this series, we envisioned a client project with constraints that wouldn’t give you the time or resources to create custom blocks. The alternative option was to use custom fields alongside the Block Bindings API.

In the previous post, you learned how to:

  • Register custom fields and bind them to block attributes.
  • Add custom block variations to bypass manually coding connected blocks.
  • Build custom meta input fields.
  • Along with a few other tips and tricks.

Much of that felt like the “setup” stage where you just get things working. This second part of the tutorial series will build atop that foundation and show you the practical steps for using custom fields in block themes.

Remember that you can always reference the GitHub repository or the Playground demo as you walk through this tutorial.

Querying posts by meta

Let’s start off with something fun: building a custom Query Loop block variation that lists book reviews by rating. A big part of this is adding a custom Rating selector in the inspector panel, as shown in this screenshot:

One of the first things I did when building the initial code for this tutorial was revisit a tutorial I published in 2022 here on the Developer Blog: Building a book review grid with a Query Loop block variation.

Back then, we didn’t have the Block Bindings API to work with, but the foundational pieces were already there. For this section of the tutorial, I’m mostly just copying and pasting from that previous work—with a few minor changes. I highly recommend giving it a read for a more in-depth dive into this technique.

Adding a rating dropdown control

Before you can query posts by meta, you need a way to select the meta you want to query by. In Part 1 of this series, you registered several meta keys. Here they are as a reminder of what they do:

  • themeslug_book_author: The book’s author name.
  • themeslug_book_rating: The user’s star rating for the book.
  • themeslug_book_length: The number of pages in the book.
  • themeslug_book_goodreads_url: The URL to the book’s page on Goodreads.com.

You could, of course, query posts by any of those fields. But let’s focus on the themeslug_book_rating meta key for this exercise. You can explore querying by other keys on your own.

First, add this code to your editor.js file in /resources/js to import the empty query.js script:

import './query';

Then open your resources/js/query.js file and import a few necessary dependencies by adding this code:

import { __ } from '@wordpress/i18n';
import { addFilter } from '@wordpress/hooks';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';

Now let’s build the Book Review panel and Rating control as shown in the above screenshot. To do this, you’ll rely on a couple of built-in WordPress components:

  • <PanelBody>: A custom panel to house any controls you want to display.
  • <SelectControl>: A dropdown select field for selecting the book rating.

Add this code to your query.js file:

const BookReviewControls = ( { props: {
	attributes,
	setAttributes
} } ) => {
	const { query } = attributes;

	return (
		<PanelBody title={ __( 'Book Review', 'themeslug' ) }>
			<SelectControl
				label={ __( 'Rating', 'themeslug' ) }
				value={ query?.bookRating || '' }
				options={ [
					{ value: '', label: '' },
					{ value: 1,  label: __( '1 Star',  'themeslug' ) },
					{ value: 2,  label: __( '2 Stars', 'themeslug' ) },
					{ value: 3,  label: __( '3 Stars', 'themeslug' ) },
					{ value: 4,  label: __( '4 Stars', 'themeslug' ) },
					{ value: 5,  label: __( '5 Stars', 'themeslug' ) }
				] }
				onChange={ ( value ) => {
					setAttributes( {
						query: {
							...query,
							bookRating: value
						}
					} );
				} }
			/>
		</PanelBody>
	);
};

Pay special attention to the onChange property and the call to setAttributes(). In particular, note that the code passes a custom attribute named bookRating with the value of the rating. You’ll use this attribute when filtering the query in future steps.

To get the custom Book Review panel and Rating control to appear for the Query Loop block, you need to filter editor.BlockEdit and append your controls to the block. 

Now add this code to your query.js file:

const withBookReviewControls = ( BlockEdit ) => ( props ) => {
	return 'core/query' === props.name ? (
		<>
			<BlockEdit {...props} />
			<InspectorControls>
			<BookReviewControls props={props} />
			</InspectorControls>
		</>
	) : (
		<BlockEdit {...props} />
	);
};

addFilter( 'editor.BlockEdit', 'core/query', withBookReviewControls );

Because the site that you’re building only has book review posts, you don’t need any other checks. This panel and control are added to all Query Loop blocks, but you can also wrap your filter in additional conditions to further restrict where the panel shows up.

Filtering the query

Adding the Rating control doesn’t actually change the queried posts. You must also add filters for when the posts are queried in the Editor and on the front end. This takes a minimal amount of PHP.

To make the meta-based query work in the Editor, you must filter the rest_post_query hook. This filter hook is run whenever posts are queried via the REST API.

Open your theme’s functions.php and add this code:

add_filter( 'rest_post_query', 'themeslug_rest_book_reviews', 10, 2 );

function themeslug_rest_book_reviews( $args, $request ) {
	$rating = $request->get_param( 'bookRating' );

	if ( $rating ) {
		$args['meta_key'] = 'themeslug_book_rating';
		$args['meta_value'] = absint( $rating );
	}

	return $args;
}

Note that the code checks for the bookRating attribute that you set in your JavaScript. If it is set, it passes the themeslug_book_rating meta key and value as query arguments.

You need to take similar steps for this to work on the front end, but it is more of a two-step process:

  • You must first filter pre_render_block to check if the bookRating attribute has been set in the block attributes.
  • If the attribute is set, you then filter query_loop_block_query_vars to add the meta key and value query arguments.

Add this code to your theme’s functions.php file:

add_filter( 'pre_render_block', 'themeslug_pre_render_block', 10, 2 );

function themeslug_pre_render_block( $pre_render, $parsed_block ) {

	if (
		isset( $parsed_block['attrs']['query']['bookRating'] )
		&& absint( $parsed_block['attrs']['query']['bookRating'] ) > 0
	) {
		add_filter(
			'query_loop_block_query_vars',
			function( $query ) use ( $parsed_block ) {
				$query['meta_key'] = 'themeslug_book_rating';
				$query['meta_value'] = absint( $parsed_block['attrs']['query']['bookRating'] );

				return $query;
			}
		);
	}

	return $pre_render;
}

From this point forward, you can query book reviews by any rating that you like. You can use it in pages, templates, and even patterns.

Patterns

Half the fun of working with blocks for me is the pattern system. It feels like we’ve gone through a lot of setup just to get to my favorite part, but it’s well worth it when all the pieces are in place to really start designing.

Before registering patterns, register a Book Reviews pattern category to house your custom patterns by adding this code to your functions.php file:

add_action( 'init', 'themeslug_register_pattern_categories' );

function themeslug_register_pattern_categories() {
	register_block_pattern_category( 'themeslug-book-review', [
		'label' => __( 'Book Reviews', 'themeslug' )
	] );
}

Book review card

Let’s say that your client wants a quick way to insert a “book review card” into their posts. There are a lot of ways that you could approach this, so feel free to experiment. But I wanted to build a card that incorporated:

  • The featured image.
  • Each of the registered custom fields and block bindings.
  • A custom quote from the book.

This was the final result:

Because this is a pattern, your client can insert it with a couple mouse clicks and enter the custom data for the specific book review.

If you recall from Part 1 of this series, you created block variations for each of the blocks that were connected to custom fields. This means that you can build this pattern directly from the editor without jumping between the Visual and Code Editors. 

So you’re not even really coding at this point—just building fun things directly in the editor.

I encourage you to build this book review card entirely on your own and register it as a pattern. But for a quick copy/paste solution, create a new file named book-review-card.php in your theme’s /patterns folder and add the following code to it:

<?php
/**
 * Title: Book Review Card
 * Slug: themeslug/book-review-card
 * Categories: themeslug-book-review
 * Viewport Width: 1376
 */
?>
<!-- wp:columns {"verticalAlignment":"center","align":"wide","style":{"spacing":{"padding":{"top":"var:preset|spacing|30","bottom":"var:preset|spacing|30","left":"var:preset|spacing|30","right":"var:preset|spacing|30"},"blockGap":{"top":"var:preset|spacing|40","left":"var:preset|spacing|30"}}},"backgroundColor":"accent"} -->
<div class="wp-block-columns alignwide are-vertically-aligned-center has-accent-background-color has-background" style="padding-top:var(--wp--preset--spacing--30);padding-right:var(--wp--preset--spacing--30);padding-bottom:var(--wp--preset--spacing--30);padding-left:var(--wp--preset--spacing--30)">

	<!-- wp:column {"verticalAlignment":"center","width":"33.33%"} -->
	<div class="wp-block-column is-vertically-aligned-center" style="flex-basis:33.33%">
		<!-- wp:post-featured-image {"aspectRatio":"3/4","style":{"border":{"radius":"0px"}}} /-->
	</div>
	<!-- /wp:column -->

	<!-- wp:column {"verticalAlignment":"center","width":"66.66%","style":{"spacing":{"blockGap":"var:preset|spacing|40"}}} -->
	<div class="wp-block-column is-vertically-aligned-center" style="flex-basis:66.66%">
		<!-- wp:group {"style":{"spacing":{"blockGap":"var:preset|spacing|10"}},"layout":{"type":"flex","orientation":"vertical"}} -->
		<div class="wp-block-group">
			<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
			<div class="wp-block-group">
				<!-- wp:paragraph -->
				<p>⭐️</p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph {"placeholder":"<?php esc_attr_e( 'Book Rating', 'themeslug' ); ?>","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_rating"}}}}} -->
				<p></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph -->
				<p><?php esc_html_e( '/ 5 Stars', 'themeslug' ); ?></p>
				<!-- /wp:paragraph -->
			</div>
			<!-- /wp:group -->

			<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
			<div class="wp-block-group">
				<!-- wp:paragraph -->
				<p><strong>📃</strong></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph {"placeholder":"<?php esc_attr_e( 'Book Length', 'themeslug' ); ?>","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_length"}}}}} -->
				<p></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph -->
				<p><?php esc_html_e( 'Pages', 'themeslug' ); ?></p>
				<!-- /wp:paragraph -->
			</div>
			<!-- /wp:group -->

			<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap"}} -->
			<div class="wp-block-group">
				<!-- wp:paragraph -->
				<p><?php esc_html_e( '✍️ Written by', 'themeslug' ); ?></p>
				<!-- /wp:paragraph -->

				<!-- wp:paragraph {"placeholder":"<?php esc_attr_e( 'Book Author', 'themeslug' ); ?>","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_author"}}}}} -->
				<p></p>
				<!-- /wp:paragraph -->
			</div>
			<!-- /wp:group -->

			<!-- wp:buttons -->
			<div class="wp-block-buttons">
				<!-- wp:button {"metadata":{"bindings":{"url":{"source":"core/post-meta","args":{"key":"themeslug_book_goodreads_url"}}}}} -->
				<div class="wp-block-button"><a class="wp-block-button__link wp-element-button"><?php esc_html_e( 'View on Goodreads ', 'themeslug' ); ?></a></div>
				<!-- /wp:button -->
			</div>
			<!-- /wp:buttons -->
		</div>
		<!-- /wp:group -->

		<!-- wp:pullquote {"textAlign":"left","style":{"typography":{"fontSize":"1.2rem"},"spacing":{"padding":{"top":"0","bottom":"0"}}},"className":"is-style-plain"} -->
		<figure class="wp-block-pullquote has-text-align-left is-style-plain" style="padding-top:0;padding-bottom:0;font-size:1.2rem"><blockquote><p></p></blockquote></figure>
		<!-- /wp:pullquote -->
	</div>
	<!-- /wp:column -->

</div>
<!-- /wp:columns -->

Note that I cleaned up the pattern file by formatting the block markup and internationalizing the text strings, as recommended in the pattern documentation.

Query grid for reviews

Similar to the previous pattern, you can now build custom Query Loop block patterns entirely from the editor while including custom fields.

Take a look at this pattern that sets the inner Post Template block to a three-column grid layout:

Nested inside the Post Template are these blocks:

  • Post Featured Image.
  • Row with two blocks:
    • Paragraph containing the “By” prefix text.
    • Book Author variation.
  • Row with three blocks:
    • Paragraph with a ⭐ emoji.
    • Book Rating variation.
    • Paragraph with the “Stars” suffix text.

Well, that’s the essence of it. I encourage you to try your hand at building it in the Editor, but also feel free to copy and paste the following code into a file named query-grid-reviews.php into your theme’s /patterns folder:

<?php
/**
 * Title: Query Grid: Book Reviews
 * Slug: themeslug/query-grid-reviews
 * Categories: themeslug-book-review
 * Viewport Width: 1376
 */
?>
<!-- wp:query {"queryId":15,"query":{"perPage":6,"pages":0,"offset":"0","postType":"post","order":"desc","orderBy":"date","author":"","search":"","exclude":[],"sticky":"","inherit":true,"taxQuery":{"category":[]}},"align":"wide","layout":{"type":"default"}} -->
<div class="wp-block-query alignwide">
	<!-- wp:group {"layout":{"type":"default"}} -->
	<div class="wp-block-group">
		<!-- wp:post-template {"align":"full","style":{"spacing":{"blockGap":"var:preset|spacing|30"}},"layout":{"type":"grid","columnCount":3}} -->
			<!-- wp:post-featured-image {"isLink":true,"aspectRatio":"3/4","style":{"border":{"radius":"0px"}}} /-->

			<!-- wp:group {"style":{"spacing":{"blockGap":"var:preset|spacing|10"}},"layout":{"type":"constrained"}} -->
			<div class="wp-block-group">
				<!-- wp:post-title {"textAlign":"center","isLink":true,"fontSize":"large"} /-->

				<!-- wp:group {"style":{"spacing":{"blockGap":"0"}},"layout":{"type":"constrained"},"fontSize":"small"} -->
				<div class="wp-block-group has-small-font-size">
					<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap","justifyContent":"center"}} -->
					<div class="wp-block-group">
						<!-- wp:paragraph -->
						<p><?php esc_html_e( 'By', 'themeslug' ); ?></p>
						<!-- /wp:paragraph -->

						<!-- wp:paragraph {"placeholder":"Book Author","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_author"}}}}} -->
						<p></p>
						<!-- /wp:paragraph -->
					</div>
					<!-- /wp:group -->

					<!-- wp:group {"style":{"spacing":{"blockGap":"0.25em"}},"layout":{"type":"flex","flexWrap":"nowrap","justifyContent":"center"}} -->
					<div class="wp-block-group">
						<!-- wp:paragraph -->
						<p>⭐️</p>
						<!-- /wp:paragraph -->

						<!-- wp:paragraph {"placeholder":"Book Rating","metadata":{"bindings":{"content":{"source":"core/post-meta","args":{"key":"themeslug_book_rating"}}}}} -->
						<p></p>
						<!-- /wp:paragraph -->

						<!-- wp:paragraph -->
						<p><?php esc_html_e( 'Stars', 'themeslug' ); ?></p>
						<!-- /wp:paragraph -->
					</div>
					<!-- /wp:group -->
				</div>
				<!-- /wp:group -->
			</div>
			<!-- /wp:group -->

		<!-- /wp:post-template -->

		<!-- wp:spacer {"height":"var:preset|spacing|40","style":{"spacing":{"margin":{"top":"0","bottom":"0"}}}} -->
		<div style="margin-top:0;margin-bottom:0;height:var(--wp--preset--spacing--40)" aria-hidden="true" class="wp-block-spacer"></div>
		<!-- /wp:spacer -->

		<!-- wp:query-pagination {"paginationArrow":"arrow","layout":{"type":"flex","justifyContent":"space-between"}} -->
			<!-- wp:query-pagination-previous /-->
			<!-- wp:query-pagination-next /-->
		<!-- /wp:query-pagination -->
	</div>
	<!-- /wp:group -->
</div>
<!-- /wp:query -->

Note that if inserting the above pattern directly into a page (or a template where it’s not meant for the main query), you will need to toggle off the Inherit query from template option in the block inspector controls.

Templates

By now, you should be realizing the process of building on top of the foundation from Part 1 is simpler with each step. That strong base means more freedom and flexibility in your designs.

With patterns out of the way, there is even less work to do as you dive into templates.

Blog Home template

For the Blog Home template, wouldn’t it be nice if you could reuse that same query grid pattern you’ve already built? You can, and that’s one of the beautiful things about the block pattern system.

Next, you’ll integrate that same pattern into your theme’s Blog Home template file so that appears like so in the Site Editor:

To output a pattern within a template, you must call the Pattern block and pass the pattern’s unique slug as an attribute:

<!-- wp:pattern {"slug":"themeslug/query-grid-reviews"} /-->

Twenty Twenty-Four already has a home.html template, so you must overwrite it by creating a new home.html file in your child theme’s /templates folder. You’ll want to add some boilerplate pieces like the header, footer, and wrapping Group block for the content area. Otherwise, you only need to call the Pattern block.

Add this code to your home.html template:

<!-- wp:template-part {"slug":"header","area":"header","tagName":"header"} /-->

<!-- wp:group {"tagName":"main","align":"full","layout":{"type":"constrained"}} -->
<main class="wp-block-group alignfull">
	<!-- wp:pattern {"slug":"themeslug/query-grid-reviews"} /-->
</main>
<!-- /wp:group -->

<!-- wp:template-part {"slug":"footer","area":"footer","tagName":"footer"} /-->

This will make your site’s blog page show the query grid pattern you built earlier. You can even repeat this process for other archive-type templates.

Single Posts template

For this particular project, I decided to keep the Single Posts template simple, giving the user control over where the book review card pattern appeared. Therefore, I removed the Post Featured Image block from Twenty Twenty-Four’s default template:

To remove the featured image, copy the /templates/single.html file from the Twenty Twenty-Four theme into your child theme in the same location. Then, remove this line of code:

<!-- wp:post-featured-image {"style":{"spacing":{"margin":{"bottom":"var:preset|spacing|40"}}}} /-->

Depending on your project’s design specs, you could explore further design options, including automatically outputting a custom card or the metadata in some other way.

A future UI with connected custom fields

This post series wouldn’t be complete without at least mentioning the ongoing work toward a built-in UI for connected custom fields in WordPress. It’s important to know when you can rely on Core tools vs. rolling a custom solution.

WordPress 6.6 will include some updates that move us closer to a fully integrated custom fields editing experience:

  • Users can edit custom field values directly from the blocks when they are connected.
  • Values are synced across multiple blocks when connected to the same field.
  • Custom fields can be updated from within a Query Loop block for the individual posts.
  • There is a new block inspector control panel that shows bound attributes (not yet editable).

At least through WordPress 6.6, you will likely still be building some custom UI components to truly flesh out the experience. Beyond that, there will likely be a day when you won’t even need to do that but for highly custom cases. To stay up to date on the Block Bindings API, follow its primary tracking ticket.

Props to @bph and @ndiego for feedback and review on this post.

Leave a Reply

Your email address will not be published. Required fields are marked *