WordPress.org

WordPress Developer Blog

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:

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.

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:

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:

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:

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:

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.

20 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.

        1. Henrik Blomgren Avatar

          Hi Birgit,

          Nope. Still doesn´t allow for a query of multiple post types. Can choose either but not make a query of a collected array.

          I haven´t seen a single plugin actually allow me to do this. Or in the rare case I do find a plugin, I think it has happend once or twice. Then the plugin costs like 60US a year to use… and that is quite expensive.

          And this is for old style of wordpress. For the Full Site Editing I haven´t seen a single plugin that allows me to pick one or more post types to make a singular query with.

          So at this point I have to find another solution for redesigning Deculture which is my site. And for future proofing it I have to remake all custom post types into normal posts so I can have my grid design on the front page.

          1. Birgit Pauli-Haack Avatar

            You might want to take a look at the Advanced Loop Block plugin by Ryan Welcher. It adds additional filter options to the query loop block as variations, among them the selection of multiple custom post types in a single query

          2. Henrik Blomgren Avatar

            Birgit…

            It´s the same plugin. It still doesn´t work with a combinated query of posts and custom post types. It only works on queries of either posts, pages or custom post types.

            It does not make one query with all the posts in one big query so instead of having to do multiple queries I can simply do one.

            It does however give me the option to filter out things from a query via meta fields or date.

            But it does not allow me to make one big query of every single thing I have on the site. If I want to display that I need x amount of queries with ONE TYPE IN EACH. Which is not what I need.

            So yeah. Same plugin you linked me to twice… will there be a third time I wonder?

          3. Birgit Pauli-Haack Avatar

            So sorry. How silly of me to suggest twice the same thing. What a waste of your time! I blame jetlag. 😅

            What I should have written: on the site of the plugin there is also a link to its support forum, where you could inquire about your needs. I am sure Ryan Welcher has a much better understanding what is and is not possible.

          4. Henrik Blomgren Avatar

            All good! It happends. Jet Lag is horrible when trying to focus. Stay safe and recover!

            It should be doable to make a custom plugin. Since there are other for the old style of wordpress that does it. But cost 60US a month… not tempting when your on a budget.

            So I look for free versions or if someone can point me in the right direction. But so far there isn´t much of a “need” for this kind of query and since FSE is still in it´s infant age? I don´t think developers are looking to solve this “issue” anytime soon.

          5. Pea Lutz Avatar

            @henrikblomgren

            In case you (or anyone else) are still looking to be able to select multiple post types in the query, the [Advanced Loop Block plugin[(https://wordpress.org/plugins/advanced-query-loop/) does have support for multiple post types. There is a new panel added titled Additional Post Types, which allows you to select multiple post types in addition to the one selected in the primary Post Type selector.

            If you look at the plugin repo, you’ll see that this is accomplished using a variation of the core query block, that adds selectors for additional query parameters.

          6. Pea Lutz Avatar

            Another option would be to use the [Newspack Blocks plugin] (https://github.com/Automattic/newspack-blocks), which doesn’t require use of the Newspack plugin or theme.

            The “Homepage Article” block likely has everything you’re looking for.

            Among other options, it has:
            – Display 1 or more post types
            – Ability to select specific posts
            – Filter by taxonomy(s)

            You would need to add support to for your custom post types. This can be accomplished by adding ‘newspack_blocks’ to using `add_post_type_support()`.

            Example:
            `add_post_type_support( {post_type}, ‘newspack_blocks’ );`

  3. Alister Cameron Avatar

    I’m trying to show recent blog posts in one query loop and then “media releases” (another post_type) in another, immediately afterwards… on the same page.

    I’d like to do this on tag/category archive pages as well as on author archive pages. But no go.

    In all cases I am stuck. If I select “Inherit query from template” option in the FSE, I have no options to change the returned post_type. If I deselect that option I can do what I want with the query, but it loses all “inheritance” from the main query.

    Any ideas here??! This was pretty basic stuff to do in PHP in pre-block/Gutenberg/FSE days, which I miss!

    I do not want to have to create custom templates for each author/category/tag, etc! That would be silly.

    I feel like I’m close, because if I could identify the execution of the second query loop, I could simply use the pre_get_posts action on the query, to chance to the custom post_type (for media releases), and I’d be done.

    But is there any way to detect that second query loop block executing after the first one?! I can’t see it.

    1. Alister Cameron Avatar

      Ok I worked it out…

      It’s a bit hacky but it works. I save the main query to a global variable, then pull what I need out of it once the second query runs. How do I know I’m in the second query? Well, I set it to a search query with a particular string (:media_releases, in this case) that I then look for, using the pre_get_posts action.

      It looks like this:

      $my_main_query = false;
      add_action( 'pre_get_posts', function( \WP_Query $q )
      {
      global $my_main_query;

      if ( $q->is_main_query() && !$q->is_feed() && !is_admin() )
      {
      $my_main_query = $q;
      }

      if ( $q->is_search() && ':media-releases' === trim( $q->get( 's' ) ) )
      {
      $main = $my_main_query;

      $q->set('post_type', 'media_release');
      $q->set('author_name', $main->query_vars['author_name']);
      $q->set('s', '');
      }
      });

      In this case I want media releases by a given author, and since I’m on an author archive page, I set it as you see above.

      Anyway, it works, and I can build off it for more tricky stuff later, while we await a more “proper” way to do it.

      1. Francesco Avatar

        Same error here. But I solved by simply adding

        allowedControls: [
        ‘order’,
        ‘author’
        ],

        so that cannot be changed inherit attributs. Obviously I set inherit false on registration.

  4. burt Avatar

    I’m trying to apply changes to the query for a query loop block, inspired by the `pre_render_block` filter above. Unfortunately, while the changes are being applied to the block that I want, they’re also being applied to all subsequent query loop blocks on the page. Given that the check within the filter is based on the passed-in `$parsed_block` (which contains data for the first block that matched the check within the `pre_render_block`-hooked function), I suspect that the code above would suffer the same issue. What am I missing here?

  5. Creative Andrew Avatar

    Hi Justin,

    I see you are doing the following check to prevent subsequent queries from being modified.

    if ( $parsed_block[‘attrs’][‘query’][‘starRating’] ) {
    }

    However, the namespace check is leaving the filter there for subsequent queries and collisions might happen. For example, if another plugin also uses the same query key (starRating) and also adds a query loop on the same page as yours.

    It just happened in my plugin:https://wordpress.org/support/topic/you-should-replace-your-query/ and I have written the following issue https://github.com/WordPress/gutenberg/issues/60295

    I would like to hear your thoughts.

    1. Andrew Y. Avatar
      Andrew Y.

      Hi Andrew, thank you so much, you saved me when my three query loop variation plugins start to producing unexpected results, your method solved my problem, you are gold! ❤️‍🔥

      1. Creative Andrew Avatar

        Hi Andrew, I am glad my comment helped you.

        @Justin Tadlock, maybe it would be important to address the issue in your blog post as people are using it as starting point.

Leave a Reply

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