humanmade / humanmade/query-filter
Filters key off `queryId`, which is neither guaranteed present nor unique
- Dominant language
- JavaScript
- Stars
- 123
- Forks
- 33
- Avg merge
- 6h 7m
- Merged PRs (30d)
- 5
Description
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`:
```
```
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).
Contributor guide
Research direction
Start by tracing filter_query_loop_block_query_vars() and pre_get_posts_transpose_query_vars(), alongside the filter block's query-{queryId}-{key} URL generation. Reproduce the missing-queryId and duplicate-queryId cases described in the issue, then establish which scope-handling behavior should be supported before implementing or testing a fix.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, php
- Domain
- backend, frontend
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100