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.