Scoping a filter to its loop relies entirely on the query block's queryId attribute: the filter blocks write query-{queryId}-{key} into the URL, filter_query_loop_block_query_vars() copies queryId into the query as query_id, and pre_get_posts_transpose_query_vars() matches the parameter prefix against it.
That attribute is neither guaranteed to exist nor guaranteed to be unique, and both edges fail badly.
1. No queryId at all — the filter silently does nothing
A query block whose markup carries a query object but no queryId:
<!-- wp:query {"query":{"perPage":10,"postType":"post","order":"asc","orderBy":"title","inherit":false}} -->
The filter renders happily and writes ?query-0-category=alpha (from $block->context['queryId'] ?? 0). But filter_query_loop_block_query_vars() only sets query_id when isset( $block->context['queryId'] ), so the loop's query has none, pre_get_posts_transpose_query_vars() returns early, and nothing is filtered.
Verified on WordPress 6.8.8 and 7.0.4: ?query-0-category=alpha and ?query-category=alpha both return the full, unfiltered set. The control looks like it works — the selection sticks, the URL changes, the results don't.
2. Two loops sharing a queryId — one control filters both
Two query blocks with the same queryId (7 in both), a taxonomy filter in the first only:
| URL |
Loop 1 |
Loop 2 |
/repro-dup/ |
all 4 posts |
all 4 posts |
/repro-dup/?query-7-category=alpha |
Alpha One, Alpha Two |
Alpha One, Alpha Two |
The second loop has no filter block, and is narrowed anyway.
This matters because duplicating a query loop block in the editor copies queryId along with everything else — see WordPress/gutenberg#55823 ("Duplicating Query Block can result in duplicate Query IDs", still open) and the closed WordPress/gutenberg#56902 ("Same queryId for all Query Loops"). Core's own enhanced pagination has the same problem, and the proposals upstream (a isUnique / non-copyable attribute property, or deriving the id from an instance id at render) have not landed.
That also looks like the mechanism behind #28, where filtering applied to every loop on the page: two loops built by duplicating one share an id, so one prefix addresses both. Filtering is correctly scoped between loops with distinct ids, which is why that report did not reproduce from hand-written fixtures.
Worth deciding
Whether the plugin should defend itself here, and how much:
- Render nothing (or an editor warning) when a filter block sits in a loop with no
queryId, rather than a control that cannot work.
- Detect two loops sharing an id in one render pass and warn, since the plugin cannot honour both.
- Or derive the scope from something the plugin controls rather than trusting the authored attribute — which needs the filter block and the loop to agree on it at render time, so it is not a small change.
The first is small and removes a silent failure. The rest deserves a design decision before anyone writes code.
Environment
Reproduced on WordPress 6.8.8 and 7.0.4, PHP 8.4, Twenty Twenty-Five, plugin at main (d8468ea).
Scoping a filter to its loop relies entirely on the query block's
queryIdattribute: the filter blocks writequery-{queryId}-{key}into the URL,filter_query_loop_block_query_vars()copiesqueryIdinto the query asquery_id, andpre_get_posts_transpose_query_vars()matches the parameter prefix against it.That attribute is neither guaranteed to exist nor guaranteed to be unique, and both edges fail badly.
1. No
queryIdat all — the filter silently does nothingA query block whose markup carries a
queryobject but noqueryId:The filter renders happily and writes
?query-0-category=alpha(from$block->context['queryId'] ?? 0). Butfilter_query_loop_block_query_vars()only setsquery_idwhenisset( $block->context['queryId'] ), so the loop's query has none,pre_get_posts_transpose_query_vars()returns early, and nothing is filtered.Verified on WordPress 6.8.8 and 7.0.4:
?query-0-category=alphaand?query-category=alphaboth return the full, unfiltered set. The control looks like it works — the selection sticks, the URL changes, the results don't.2. Two loops sharing a
queryId— one control filters bothTwo query blocks with the same
queryId(7in both), a taxonomy filter in the first only:/repro-dup//repro-dup/?query-7-category=alphaThe second loop has no filter block, and is narrowed anyway.
This matters because duplicating a query loop block in the editor copies
queryIdalong with everything else — see WordPress/gutenberg#55823 ("Duplicating Query Block can result in duplicate Query IDs", still open) and the closed WordPress/gutenberg#56902 ("Same queryId for all Query Loops"). Core's own enhanced pagination has the same problem, and the proposals upstream (aisUnique/ non-copyable attribute property, or deriving the id from an instance id at render) have not landed.That also looks like the mechanism behind #28, where filtering applied to every loop on the page: two loops built by duplicating one share an id, so one prefix addresses both. Filtering is correctly scoped between loops with distinct ids, which is why that report did not reproduce from hand-written fixtures.
Worth deciding
Whether the plugin should defend itself here, and how much:
queryId, rather than a control that cannot work.The first is small and removes a silent failure. The rest deserves a design decision before anyone writes code.
Environment
Reproduced on WordPress 6.8.8 and 7.0.4, PHP 8.4, Twenty Twenty-Five, plugin at
main(d8468ea).