Building a book review grid with a Query Loop block variation


WordPress 6.1 introduced new methods for extending the Query Loop block.  This is a significant milestone because it puts a lot of functionality in the hands of plugin developers at little cost, at least in terms of code. Instead of building custom blocks to query posts, extenders need to only filter the existing functionality in core WordPress.

The Query Loop block is a workhorse in building websites out of blocks.  It is the foundation for displaying content from posts, pages, and other custom post types (CPTs).

Before WordPress 6.1, its primary use case was displaying a limited subset of what was previously possible when compared to its PHP counterpart: WP_Query.  This meant that developers, particularly when handling data for CPTs, were often building entire custom blocks on their own.

Today, extenders can build atop a solid foundation for outputting nearly any type of content with minimal code on their part.  The changes in version 6.1 allow plugin authors to skip the block development aspect and extend the built-in Query Loop block.

A few examples of the ways this can be used include:

  • Displaying a grid of products by a price meta field.
  • Business directory listing businesses by location.
  • Leaderboard for a peer-to-peer fundraiser.
  • Outputting book reviews by rating.

For this walk-through, you will learn how to tackle the last item in that list: listing book review posts. You will build a WordPress plugin from start to finish.  The result will be a Query Loop block variation that looks similar to the following screenshot:

WordPress post editor with three book review posts, each with a featured image and the title below.

The basic methods in this simple tutorial can also be applied to more complex projects.

Requirements

Aside from some baseline JavaScript development knowledge and familiarity with block development, you should have these tools available on your machine:

  • Node/NPM Development Tools
  • WordPress Development Site
  • Code Editor

For further information on setting these up, visit the Development Environment guide in the Block Editor Handbook.

Setting up content

For this tutorial, assume that you have a client who likes to write book reviews from time to time and wants to show off the latest reviews in various places on their site, such as on a custom page. The client has a custom category titled “Book Reviews” and a few posts already written.

Recreate this scenario in your development environment.

WordPress posts management screen where a Book Reviews category has been selected.  It shows three posts in the table.
Posts categorized as Book Reviews.

First, add a new “Book Reviews” category and make note of the category ID. You will need this later. Then, create at least three example posts that are assigned to this category and give each a featured image.

Plugin setup

Create a new plugin in your wp-content/plugins directory. Name it something like book-reviews-grid (the exact name is not particularly important). Now, add the following files with this specific structure:

book-reviews-grid
    /index.php
    /package.json
    /src
        /index.js

You can change index.php to whatever you want. It is your plugin’s primary PHP file.

PHP setup

In your plugin’s primary PHP file, add the plugin header with some basic information:

<?php
/**
 * Plugin Name:       Book Reviews Grid
 * Version:           1.0.0
 * Requires at least: 6.1
 * Requires PHP:      7.4
 */

// Additional code goes here…

This will be the only PHP file you will need for this tutorial, and all PHP code will go into it.

Build process setup

First, open your package.json file and add the start script. This will be used for the build process. You can add other fields, such as name and description if you want.

{
    "scripts": {
        "start": "wp-scripts start"
    }
}

This tutorial requires the @wordpress/scripts package, which can be installed via the command line:

npm install @wordpress/scripts --save-dev

Once you have everything set up, type the following in your command line program:

npm run start

Aside from the required start command, you can find all of the available scripts via the @wordpress/scripts package and add them to your package.json if needed.

Building a simple Query Loop variation

The process for registering simple Query Loop variation (one without any custom query variable integration) requires only a few dozen lines of code. You must import registerBlockVariation and use it to register the variation.

JavaScript: Building the variation

At the top of your src/index.js file, add the following line of code:

import { registerBlockVariation } from '@wordpress/blocks';

Now, you need two pieces of information. First, decide on a unique name for the variation. book-reviews will work for now. The second bit of data needed is the ID of the “Book Reviews” category that you created earlier in this walk-through.

Take both of those values and assign them to constants, as shown in the following snippet:

const VARIATION_NAME     = 'book-reviews';
const REVIEW_CATEGORY_ID = 8; // Assign custom category ID.

Now, it is time to register the variation. First, add a few basic properties, such as the name, title, and more, as shown in the following code block:

registerBlockVariation( 'core/query', {
    name: VARIATION_NAME,
    title: 'Book Reviews',
    icon: 'book',
    description: 'Displays a list of book reviews.',
    isActive: [ 'namespace' ],
    // Other variation options...
} );

There are two necessary options to set when registering the variation for the core/query block:

  • The name property should match your unique variation name.
  • The isActive property should be an array with the namespace attribute (you will define this attribute in the next step).

From this point, the variation is mostly customizable, but let’s walk through this one step at a time, adding new options for the variation. The next piece to build is the variation’s attributes. Attributes can match any that the Query Loop block accepts.

The one extra required attribute is namespace. It must match the variation name so that WordPress will be able to check whether it is the active variation.

For this tutorial, the variation displays the latest six posts within the “Book Reviews” category. It also has a wide layout in a three-column grid. Feel free to customize the options to your liking.

registerBlockVariation( 'core/query', {
    // ...Previous variation options.
    attributes: {
        namespace: VARIATION_NAME,
        query: {
            postType: 'post',
            perPage: 6,
            offset: 0,
            taxQuery: {
                category: [ REVIEW_CATEGORY_ID ]
            }
        },
        align: 'wide',
        displayLayout: {
            type: 'flex',
            columns: 3
        }
    },
    // Other variation options...
} );

Developers can also choose which of the default WordPress controls are available by setting the allowedControls array (by default, all controls are shown). These appear as block options in the interface. For a full list of controls and their definitions, visit the allowed controls section in the Block Editor Handbook.

The following example, adds the order and author controls:

registerBlockVariation( 'core/query', {
    // ...Previous variation options.
    allowedControls: [
        'order',
        'author'
    ],
    // Other variation options...
} );

Finally, you should add some inner blocks for the variation. The first top-level block should always be core/post-template. The following code snippet uses the core Post Featured Image and Post Title blocks with no customizations, but feel free to add other blocks and set default options for each block.

registerBlockVariation( 'core/query', {
    // ...Previous variation options.
    innerBlocks: [
        [
            'core/post-template',
            {},
            [
                [ 'core/post-featured-image' ],
                [ 'core/post-title' ]
            ],
        ]
    ]
} );

For a full overview of the available options, visit the following resources:

PHP: Loading the JavaScript

With the base JavaScript handled, now you must load the JavaScript file itself.  The build process will generate two files:

  • build/index.js: The JavaScript file to be loaded.
  • build/index.asset.php: An array of dependencies and a version number for the script.

The following code snippet should be added to index.php. It gets the generated asset file if it exists and loads the script in the editor:

add_action( 'enqueue_block_editor_assets', 'myplugin_assets' );

function myplugin_assets() {

    // Get plugin directory and URL paths.
    $path = untrailingslashit( __DIR__ );
    $url  = untrailingslashit( plugins_url( '', __FILE__ ) );

    // Get auto-generated asset file.
    $asset_file = "{$path}/build/index.asset.php";

    // If the asset file exists, get its data and load the script.
    if ( file_exists( $asset_file ) ) {
        $asset = include $asset_file;

        wp_enqueue_script(
            'book-reviews-variation',
            "{$url}/build/index.js",
            $asset['dependencies'],
            $asset['version'],
            true
        );
    }
}

With this code in place, you should be able to add the “Book Reviews” variation via the block inserter or a slash command (e.g. /book reviews) in the block editor:

WordPress post editor with the block inserter open.  It is displaying the Book Reviews block variation.
Block inserter showing Book Reviews variation.

You could stop at this point of the tutorial if your variation doesn’t require any customizations to the queried posts beyond what the core Query Loop block handles out of the box.

Integrating post metadata into the variation

Now, let’s dive into a slightly more advanced scenario that builds upon the existing code.  You will build a control for users to display book reviews based on a post meta key and value pair.

The crucial aspects for this to work are the filter hooks that WordPress provides.  Once you learn how to use these, you can expand them to custom post types and other real-world projects.

Setting up post metadata

You will need a post meta key of rating with a meta value between 1 and 5 that is attached to one or more of the posts in the Book Reviews category.  The easiest way to do this is to use the Custom Fields panel on the post editing screen.

Note: If you do not see the Custom Fields panel, you can enable it from Options (⋮ icon) > Preferences > Panels menu in the editor.

Head back to each of your book review posts and add in a rating value, as shown in the following screenshot:

WordPress post editor with the Custom Fields panel shown at the bottom of the screen. The panel shows a rating field with a value of 5.
Adding a “rating” custom field

In a real-world project, you will likely want to build proper form fields for the end-user to easily select a star rating.  However, that is outside the scope of this tutorial.

JavaScript: Adding block variation controls

To add extra controls to your custom Query Loop variation, you will need to import a few additional modules into your script. Add the following code to the top of your src/index.js file. You will use these as you build out the remainder of the functionality.

// ...Previous imports.
import { addFilter } from '@wordpress/hooks';
import { InspectorControls } from '@wordpress/block-editor';
import { PanelBody, SelectControl } from '@wordpress/components';

Next, build a quick helper function for determining if a block, based on its props, matches your variation. You will use this later in the code. Most of the work has already been done because you previously set up a variation name constant and namespace to check against.

const isBookReviewsVariation = ( props ) => {
    const {
        attributes: { namespace }
    } = props;

    return namespace && namespace === VARIATION_NAME;
};

Now, it is time to build a component for displaying a custom block panel section. You can add multiple fields here later. For now, it will house a form field for selecting a star rating.

The following code uses the SelectControl component to create a dropdown select of each of the available ratings. This could just as easily be a radio list, button group, or an entirely custom React component. It is up to you.

The vital piece of this code is saving the star rating value to props.attributes.query.starRating. You will need this later to modify the posts query.

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

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

Once you’ve built out the custom panel section and control, you must filter the Query Loop (core/query) block to add in custom controls. That is where the earlier isBookReviewsVariation helper function comes in. You will pass it the block’s props to determine if it is your custom variation. If it matches, add your controls.

export const withBookReviewControls = ( BlockEdit ) => ( props ) => {

    return isBookReviewsVariation( props ) ? (
        <>
            <BlockEdit {...props} />
            <InspectorControls>
                <BookReviewControls props={props} />
            </InspectorControls>
        </>
    ) : (
        <BlockEdit {...props} />
    );
};

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

At this point, you should have a visible section titled “Book Review” with a “Rating” select dropdown inside of it when your variation is in use, as shown in the following screenshot:

WordPress post editor showing a grid of three posts with their featured images and titles.  In the sidebar, a star rating dropdown is shown.
Book Reviews with rating field

Selecting a rating should not change the queried posts at this point. There are still a couple of filters left to add to make it work.

PHP: Filtering the queried posts

You must add two PHP filters to make this work for both the editor and front end. Then, you will have a fully-working Query Loop variation with post meta integration.

The first filter will go on the rest_{$post_type}_query hook. Because you are building this with the “post” post type, that hook name becomes rest_post_query.

This filter will run on every query for that type, so you need to check for your custom query parameter (starRating) before making any changes. You can check for this via the callback function’s $request parameter, which provides an instance of the WP_REST_Request class. Use its get_param() method to check for the custom query parameter.

If the starRating value is set, you only need to pass the meta key and value back as query arguments. To do this, add the following code to your plugin’s primary PHP file:

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

function myplugin_rest_book_reviews( $args, $request ) {

    $rating = $request->get_param( 'starRating' );

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

    return $args;
}

Now, test your previously-built rating control in the editor. As shown in the following screenshot, only the posts with the selected rating are queried:

WordPress post editor that shows two posts in a grid, each with a featured image and title. In the sidebar, a 5-star rating is selected.
Book Reviews with a 5-star rating.

While this works in the editor, you also need to use the pre_render_block hook to run some custom code when the block is rendered on the front end. Then, you will need to nest a second filter inside of that callback on the query_loop_block_query_vars hook using an anonymous function. The reason for this is that you need access to the parsed block attributes.

If it sounds a little convoluted, it is. Ideally, there will be a slightly less complex method for doing this in the future.

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

function myplugin_pre_render_block( $pre_render, $parsed_block ) {

    // Determine if this is the custom block variation.
    if ( 'book-reviews' === $parsed_block['attrs']['namespace'] ) {

        add_filter(
            'query_loop_block_query_vars',
            function( $query, $block ) use ( $parsed_block ) {

                // Add rating meta key/value pair if queried.
                if ( $parsed_block['attrs']['query']['starRating'] ) {
                    $query['meta_key'] = 'rating';
                    $query['meta_value'] = absint( $parsed_block['attrs']['query']['starRating'] );
                }

                return $query;
            },
            10,
            2
        );
    }

    return $pre_render;
}

Now, you should have the same queried posts on the front end of the site as shown in the editor.

With this foundation in place, you can extend this to other projects. Essentially, you can build any type of query that you need with a few filters and custom controls. While this tutorial may seem a bit long, it contains fewer than 200 lines of code in total, and that is a major win in comparison to building out fully-fledged blocks.

What types of Query Loop block variations do you have in mind?

Props to @bph, @mburridge, and @webcommsat for technical and editorial feedback.

6 responses to “Building a book review grid with a Query Loop block variation”

  1. Henrik Blomgren Avatar

    Lovely and interesting. But… it´s all about normal posts. I run a website that has about 300+ reviews in a custom post type and about 100 normal posts.

    The way I design my front page is to make a custom query containing both the custom post type and my normal posts into one big query.

    This is something I´ve noticed in WP. There isn´t really a good plugin or way to handle a custom query for FSE that meets my needs of a custom query containing multiple post types. Not just the normal post type or a single custom post type. I want to mix EVERYTHING into one big query and then display it as I want.

    So I would love to see query loop variation where you actually mix different post types. Pages, custom post types, normal posts? Instead of just having to do multiple query loops with 1 kind of type of posts.

    1. Justin Tadlock Avatar

      That’s actually a great question because I believe the REST API only handles a single post type. I don’t have a complete answer to this question (maybe someone else can chime in), but maybe I can at least point you in the right direction.

      I believe you’d have to filter something like rest_{$post_type}_query to handle multiple post types and also rest_endpoints, making sure each of those endpoints for {$post_type} accepts an array for the type.

      1. Henrik Blomgren Avatar

        Hi Justin,

        Thank you for the answer and sorry for my late reply. I wanted to get notified about answers to my comment but I only got a confirmation for getting notified whenever there was a new comment. So I haven´t been here for a while.

        And this is where my developer knowledge hits a wall. As I can barely understand how to use this. But it looks promising as from what I kind of understand with the examples shown and such is that I can use arguments for the WP_query and then turn it into a query for the rest api.

        But how to implement it… that is currently beyond me as I´m more of a designer than a developer and it sucks not being able to do everything you want.

        Once again thank you for the answer. I can´t proceed further on my own at this point in time sadly. But I learned more than I knew beforehand so it´s a win.

  2. Tristan Bailey Avatar

    (First thanks for the post, it does take work to think of hooks at multiple points) Henrik, custom post types are an option you can change in the Query so either make similar code to here but the ‘post_type’ = “book” (or postType if in the js part) or what you have.
    It takes a little more work to query WP to give you a selectable list so easier to start with that part hard coded.

    1. Henrik Blomgren Avatar

      Yes Tristan, I know I can change the post type. That is how the loop block works. It works splendid if I want to make a single query of either normal posts OR a custom post type.

      It does however, NOT work if I want to make a query consisting of BOTH normal posts and a custom post type. Which was what I was asking about.

      Back when it was php it´s as easy as putting a array(‘post’, ‘cpt-name’); but with this block editor? Doesn´t work as easy.

      Going into the editor and putting up an array in ‘post_type’ does not work. I tried that.

      So it´s easy when it´s singular, but as of the writing and to my knowledge there is no support for MULTIPLE types.

      1. Birgit Pauli-Haack Avatar

        Hi Hendrik, you might want to look into the Advanced Query Loop plugin, that might already allow for multiple post types. If not in the current version, I believe it’s on the roadmap.

Leave a Reply

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