WordPress Query Loop Block: Custom Queries and Extending with PHP

The Query Loop block is the FSE replacement for WP_Query-based PHP templates. It is highly flexible out of the box, but to power custom post types, meta-based ordering, or relationship queries you must hook into query_loop_block_query_vars to modify the query and optionally register a custom block variation to expose the new controls in the editor.

Problem: The Query Loop block in the WordPress block editor provides basic post listing functionality, but applying custom query parameters — meta queries, tax queries, post status filters, or JOIN-based sorting — is not possible through the block's UI.

Solution: Extend the Query Loop block with a custom block variation: register it with registerBlockVariation() and add a server-side filter using pre_render_block or the query_loop_block_query_vars filter to merge custom query args. For complex custom queries, register a custom REST endpoint and render with a dynamic block using render_callback.


The code below modifies the Query Loop block's underlying WP_Query via a filter, registers a custom block variation for a "Portfolio" post type, and adds a server-side rendered block attribute to pass extra query parameters from the editor UI.


context['namespace'] ?? '' ) !== 'my-plugin/portfolio' ) {
            return $query;
        }

        $query['post_type']      = 'portfolio';
        $query['posts_per_page'] = (int) ( $block->attributes['postsToShow'] ?? 6 );
        $query['meta_key']       = '_featured_order';
        $query['orderby']        = 'meta_value_num';
        $query['order']          = 'ASC';

        // Support a taxonomy filter stored as a block attribute
        $term_id = (int) ( $block->attributes['filterTermId'] ?? 0 );
        if ( $term_id ) {
            $query['tax_query'] = [ [
                'taxonomy' => 'portfolio_category',
                'field'    => 'term_id',
                'terms'    => [ $term_id ],
            ] ];
        }

        return $query;
    }, 10, 3
);

// 2. Register a Query Loop block variation (PHP — also possible in JS)
add_action( 'init', function () {
    register_block_style(
        'core/query',
        [
            'name'  => 'portfolio-grid',
            'label' => __( 'Portfolio Grid', 'my-plugin' ),
        ]
    );
} );

// 3. Expose block variation via JS (enqueue in editor)
add_action( 'enqueue_block_editor_assets', function () {
    wp_enqueue_script(
        'my-plugin-query-variation',
        plugin_dir_url( __FILE__ ) . 'js/query-variation.js',
        [ 'wp-blocks', 'wp-element' ],
        '1.0.0',
        true
    );
} );


// js/query-variation.js
const { registerBlockVariation } = wp.blocks;

registerBlockVariation( 'core/query', {
    name:        'my-plugin/portfolio',
    title:       'Portfolio Query',
    description: 'Display portfolio items with category filter.',
    isDefault:   false,
    icon:        'portfolio',
    category:    'theme',
    attributes: {
        namespace:    'my-plugin/portfolio',
        query: {
            postType:     'portfolio',
            perPage:      6,
            inherit:      false,
        },
    },
    // Mark this variation as active when namespace matches
    isActive: ( blockAttrs ) => blockAttrs.namespace === 'my-plugin/portfolio',
    innerBlocks: [
        [ 'core/post-template', {}, [
            [ 'core/post-featured-image' ],
            [ 'core/post-title' ],
            [ 'core/post-excerpt' ],
        ] ],
        [ 'core/query-pagination' ],
    ],
} );


NOTE: Always check $block->context['namespace'] inside query_loop_block_query_vars before modifying the query — without this guard your filter will modify every Query Loop block on every page, including those in the Site Editor and widget areas that you did not intend to change.