From 9e98fa2f37d3d20af65dd6243d0c310e0f7579e5 Mon Sep 17 00:00:00 2001 From: Matthew Haines-Young Date: Tue, 6 Oct 2026 17:32:14 +0100 Subject: [PATCH 1/4] Presets: carry the block instance in the preset context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A preset callback could see query vars and a small context array, but never the WP_Block, so it could not read block context: no termId or taxonomy inside a Term Template, no postId, nothing core puts there. The frontend filter is handed the block, reads its context to find the preset name, and then throws it away. Put it in the context array as 'block_instance'. The callback signature does not change, so every preset already written keeps working. Not 'block': that key is taken, and holds perPage and page. It is misnamed, but presets read it, so renaming it is breaking and stays out of scope — which is exactly why the new key says "instance" out loud. modify_rest_query_for_preset() sets it to null rather than omitting it, so the context has one shape and a preset can write $context['block_instance']?-> without an isset dance. It is null there because the editor preview queries the collection endpoint instead of rendering a block: there is no instance. A preset leaning on block context will differ between preview and frontend, and the docs say so rather than implying otherwise. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 9 +- README.md | 47 +++++++- inc/query-presets.php | 41 ++++--- package.json | 2 +- tests/php/query-presets-test.php | 191 +++++++++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 19 deletions(-) create mode 100644 tests/php/query-presets-test.php diff --git a/CLAUDE.md b/CLAUDE.md index 94fda98..1ce25fa 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -102,7 +102,9 @@ The plugin provides a PHP API for registering custom query presets that can be s 'related_articles', // Unique identifier 'Related Articles', // Human-readable label function( $query_vars, $context ) { - // $context includes: post_id, is_rest, block (perPage, page) + // $context includes: post_id, is_rest, block_instance, block (perPage, page) + // $context['block'] is pagination metadata, NOT the block — the block is + // $context['block_instance'], e.g. $context['block_instance']?->context['termId'] ?? 0 // Modify and return $query_vars return $query_vars; } @@ -113,9 +115,11 @@ The plugin provides a PHP API for registering custom query presets that can be s 1. Presets are registered via PHP callbacks that receive query args and context 2. The preset selector appears in the block editor when presets are registered 3. REST API hooks are automatically added for all public post types via `rest_{$post_type}_collection_params` and `rest_{$post_type}_query` -4. Frontend queries are modified via `query_loop_block_query_vars` filter +4. Frontend queries are modified via `query_loop_block_query_vars` filter, which already holds the `WP_Block` and puts it in the context as `block_instance` 5. The selected preset is stored in `query.hmPreset` block attribute +The callback signature is unchanged — the block travels in the context array that was already there. `block_instance` is null on REST (`modify_rest_query_for_preset`): the editor preview queries the collection endpoint and never renders the block, so there is no instance to carry. The key is set explicitly in both paths, so the context has one shape. + ## Key Files - `hm-query-loop.php` - Main plugin file with all PHP hooks and query modification logic @@ -124,6 +128,7 @@ The plugin provides a PHP API for registering custom query presets that can be s - `docs/query-caching.md` - Why exclusions are applied in PHP, and what is left to do - `src/index.js` - Block filters for adding inspector controls and editor preview behavior - `tests/php/deferred-exclusions-test.php` - Unit tests for the exclusion planner +- `tests/php/query-presets-test.php` - Unit tests for the preset callback contract - `tests/e2e/fixtures.js` - Playwright test fixtures for WordPress admin - `tests/e2e/posts-per-page.spec.js` - E2E tests for posts per page functionality - `tests/e2e/query-presets.spec.js` - E2E tests for query presets diff --git a/README.md b/README.md index be173c4..1af38f1 100644 --- a/README.md +++ b/README.md @@ -147,9 +147,11 @@ add_action( 'init', function() { 'Related Articles', // Label shown in dropdown function( $query_vars, $context ) { // $context includes: - // - post_id: Current post ID (useful for related content) + // - post_id: The post the block is rendering for // - is_rest: Boolean, true when called from REST API (editor) - // - block: Array with perPage and page values + // - block_instance: The WP_Block being rendered, or null on REST + // - block: Array with perPage and page values. Despite the name this is + // pagination metadata, not the block — that is block_instance. $related_ids = get_post_meta( $context['post_id'], 'related_posts', true ); @@ -164,6 +166,47 @@ add_action( 'init', function() { }); ``` +### Reading block context + +`$context['block_instance']` is the `WP_Block` being rendered, so a preset can read +anything core puts in its context — `postId`, or the `termId` and `taxonomy` a Term +Template provides to the blocks inside it: + +```php +\HM\QueryLoop\QueryPresets\register_query_preset( + 'posts_in_this_term', + 'Posts in this term', + function( $query_vars, $context ) { + $term_id = $context['block_instance']?->context['termId'] ?? 0; + + if ( ! $term_id ) { + return $query_vars; + } + + $query_vars['tax_query'] = [ + [ + 'taxonomy' => $context['block_instance']->context['taxonomy'] ?? 'category', + 'terms' => [ $term_id ], + ], + ]; + + return $query_vars; + } +); +``` + +It is named `block_instance` because `$context['block']` was there first and is not +the block: it holds `perPage` and `page`. Renaming that would break presets already +reading it, so both keys stay. + +`block_instance` is null on REST requests. The editor preview fetches posts from the +collection endpoint rather than rendering the block, so there is no block instance to +carry — a preset that depends on block context will behave differently in the editor +preview to the frontend. Code for that rather than expecting a block. The key is +always present, so `?->` is enough and no `isset()` is needed. + +The callback signature is unchanged, so every existing preset keeps working. + **Available Functions:** - `\HM\QueryLoop\QueryPresets\register_query_preset( $name, $label, $callback )` - Register a preset diff --git a/inc/query-presets.php b/inc/query-presets.php index fa28f0c..745aca0 100644 --- a/inc/query-presets.php +++ b/inc/query-presets.php @@ -10,6 +10,8 @@ namespace HM\QueryLoop\QueryPresets; +use WP_Block; + /** * Registered query presets. * @@ -24,7 +26,11 @@ * @param string $label Human-readable label for the preset (e.g., 'Related Articles'). * @param callable $callback Function that receives query args and block context, returns modified query args. * Signature: function(array $query_vars, array $context): array - * Context includes: 'post_id' (current post), 'block' (block attributes), 'is_rest' (bool). + * Context includes: 'post_id' (the post the block is rendering for), + * 'is_rest' (bool), 'block_instance' (the WP_Block, or null), and 'block' — + * which despite the name is pagination metadata ('perPage', 'page'). + * 'block_instance' is null on REST requests, because the editor preview + * queries the collection endpoint directly: no block is being rendered. * @return bool True on success, false if preset already exists. */ function register_query_preset( string $name, string $label, callable $callback ): bool { @@ -94,7 +100,8 @@ function get_query_preset( string $name ): ?array { * * @param string $name Preset identifier. * @param array $query_vars Current query arguments. - * @param array $context Additional context (post_id, block, is_rest). + * @param array $context Additional context (post_id, is_rest, block_instance, block). Note + * that 'block' is pagination metadata; 'block_instance' is the WP_Block. * @return array Modified query arguments. */ function apply_query_preset( string $name, array $query_vars, array $context = [] ): array { @@ -182,9 +189,12 @@ function modify_rest_query_for_preset( array $args, \WP_REST_Request $request ): } $context = [ - 'post_id' => $request->get_param( 'post_id' ) ?? get_the_ID() ?? 0, - 'is_rest' => true, - 'block' => [ + 'post_id' => $request->get_param( 'post_id' ) ?? get_the_ID() ?? 0, + 'is_rest' => true, + // Set explicitly, so the context has one shape and presets need no isset dance. + // REST renders no block: the editor preview queries the collection endpoint. + 'block_instance' => null, + 'block' => [ 'perPage' => $request->get_param( 'per_page' ), ], ]; @@ -195,14 +205,14 @@ function modify_rest_query_for_preset( array $args, \WP_REST_Request $request ): /** * Filter query vars for the Query Loop block on the frontend. * - * @param array $query_vars Existing query variables. - * @param \WP_Block $block Block instance. - * @param int $page Current page number. + * @param array $query_vars Existing query variables. + * @param WP_Block $block Block instance. + * @param int $page Current page number. * @return array Modified query variables. */ -function filter_query_loop_block_query_vars( array $query_vars, \WP_Block $block, int $page ): array { - $context = $block->context ?? []; - $query_attr = $context['query'] ?? []; +function filter_query_loop_block_query_vars( array $query_vars, WP_Block $block, int $page ): array { + $block_context = $block->context ?? []; + $query_attr = $block_context['query'] ?? []; $preset_name = $query_attr['hmPreset'] ?? ''; if ( empty( $preset_name ) ) { @@ -210,9 +220,12 @@ function filter_query_loop_block_query_vars( array $query_vars, \WP_Block $block } $context = [ - 'post_id' => get_the_ID() ?? 0, - 'is_rest' => false, - 'block' => [ + 'post_id' => get_the_ID() ?? 0, + 'is_rest' => false, + // Presets need the block itself to reach core's context — termId and taxonomy + // inside a Term Template, postId, and so on. 'block' below is not that. + 'block_instance' => $block, + 'block' => [ 'perPage' => $query_vars['posts_per_page'] ?? get_option( 'posts_per_page', 10 ), 'page' => $page, ], diff --git a/package.json b/package.json index dfec269..7355ce2 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "lint:css:fix": "wp-scripts lint-style --fix", "format": "wp-scripts format", "wp-env": "wp-env", - "test:php": "php tests/php/deferred-exclusions-test.php", + "test:php": "php tests/php/deferred-exclusions-test.php && php tests/php/query-presets-test.php", "test:e2e": "wp-scripts test-playwright", "test:e2e:debug": "wp-scripts test-playwright --debug", "test:e2e:watch": "wp-scripts test-playwright --watch" diff --git a/tests/php/query-presets-test.php b/tests/php/query-presets-test.php new file mode 100644 index 0000000..e015e72 --- /dev/null +++ b/tests/php/query-presets-test.php @@ -0,0 +1,191 @@ +name = $name; + $this->context = $context; + } + } + + class WP_REST_Request { + private $params = []; + + public function __construct( $params = [] ) { + $this->params = $params; + } + + public function get_param( $key ) { + return $this->params[ $key ] ?? null; + } + } + + function add_filter( ...$args ) {} + + function add_action( ...$args ) {} + + function get_option( $name, $default = false ) { + return $default; + } + + function get_the_ID() { + return $GLOBALS['current_post_id']; + } + + function _doing_it_wrong( ...$args ) {} + + function __( $text, $domain = null ) { + return $text; + } + + function esc_html__( $text, $domain = null ) { + return $text; + } + + function esc_attr( $text ) { + return $text; + } +} + +namespace HM\QueryLoop\Tests\Presets { + + use WP_Block; + use WP_REST_Request; + + use function HM\QueryLoop\QueryPresets\apply_query_preset; + use function HM\QueryLoop\QueryPresets\filter_query_loop_block_query_vars; + use function HM\QueryLoop\QueryPresets\modify_rest_query_for_preset; + use function HM\QueryLoop\QueryPresets\register_query_preset; + + require_once dirname( __DIR__, 2 ) . '/inc/query-presets.php'; + + $failures = 0; + $checks = 0; + + /** + * Assert that two values match. + * + * @param string $label What is being checked. + * @param mixed $actual The value produced. + * @param mixed $expected The value wanted. + * @return void + */ + function check( string $label, $actual, $expected ): void { + global $failures, $checks; + + $checks++; + + if ( $actual === $expected ) { + printf( "ok %s\n", $label ); + return; + } + + $failures++; + printf( + "FAIL %s\n expected: %s\n actual: %s\n", + $label, + var_export( $expected, true ), + var_export( $actual, true ) + ); + } + + /** + * The context each preset invocation was handed, keyed by preset name. + * + * @var array + */ + $seen_context = []; + + // Records the whole context, so the tests can inspect any key of it. + register_query_preset( + 'records_context', + 'Records Context', + function ( array $query_vars, array $context ): array { + $GLOBALS['seen_context']['records_context'] = $context; + return $query_vars; + } + ); + + // Reads block context the way a preset inside a Term Template would. + register_query_preset( + 'reads_term', + 'Reads Term', + function ( array $query_vars, array $context ): array { + $query_vars['term_id'] = $context['block_instance']?->context['termId'] ?? 0; + return $query_vars; + } + ); + + $block = new WP_Block( + 'core/post-template', + [ + 'query' => [ 'hmPreset' => 'records_context' ], + 'termId' => 77, + 'taxonomy' => 'category', + ] + ); + + // The filter holds the block already; it must put it in the context it builds. + filter_query_loop_block_query_vars( [ 'posts_per_page' => 5 ], $block, 2 ); + $frontend = $seen_context['records_context']; + + check( 'frontend: context carries the block instance', $frontend['block_instance'], $block ); + check( 'frontend: is_rest false', $frontend['is_rest'], false ); + + // 'block' is misnamed but load-bearing: renaming it would break existing presets. + check( 'frontend: block stays pagination metadata', $frontend['block'], [ 'perPage' => 5, 'page' => 2 ] ); + + // REST renders no block, and says so with the key present rather than absent. + modify_rest_query_for_preset( [], new WP_REST_Request( [ 'hmPreset' => 'records_context' ] ) ); + $rest = $seen_context['records_context']; + + check( 'REST: block_instance key exists', array_key_exists( 'block_instance', $rest ), true ); + check( 'REST: block_instance is null', $rest['block_instance'], null ); + + // Reaching core's block context is the whole point of carrying the instance. + $term_block = new WP_Block( + 'core/post-template', + [ + 'query' => [ 'hmPreset' => 'reads_term' ], + 'termId' => 77, + ] + ); + $term_vars = filter_query_loop_block_query_vars( [ 'posts_per_page' => 5 ], $term_block, 1 ); + check( 'preset reads termId through block_instance', $term_vars['term_id'], 77 ); + + // Applied directly, the context passes through untouched. + apply_query_preset( 'records_context', [], [ 'post_id' => 5, 'block_instance' => null ] ); + check( 'apply_query_preset: context passed through', $seen_context['records_context'], [ 'post_id' => 5, 'block_instance' => null ] ); + + // An unregistered preset is still a no-op. + check( 'unknown preset: query vars untouched', apply_query_preset( 'nope', [ 'orderby' => 'date' ], [] ), [ 'orderby' => 'date' ] ); + + printf( "\n%d checks, %d failures\n", $checks, $failures ); + + exit( $failures > 0 ? 1 : 0 ); +} From 8d8e56071c0c30c0d3cef31f7c7871aed9d9cf89 Mon Sep 17 00:00:00 2001 From: Matthew Haines-Young Date: Tue, 6 Oct 2026 17:32:14 +0100 Subject: [PATCH 2/4] Presets: take post_id from the block, not the global loop filter_query_loop_block_query_vars() built 'post_id' from get_the_ID(), which reads whatever post the global loop is on. The block's own context already holds the post core resolved for that block, and the function reads that context one statement earlier to find the preset name. Prefer $block->context['postId'], falling back to get_the_ID(). On a singular template the two agree; they diverge wherever the global post is not what the block is rendering for. This is a behaviour change, not a refactor: a preset keying off 'post_id' in a context where the two differ will now get a different post. That is the post it should have had, but it is worth saying plainly rather than finding out in production. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 2 ++ README.md | 3 ++- inc/query-presets.php | 5 ++++- tests/php/query-presets-test.php | 17 +++++++++++++++++ 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 1ce25fa..0e38e6b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -120,6 +120,8 @@ The plugin provides a PHP API for registering custom query presets that can be s The callback signature is unchanged — the block travels in the context array that was already there. `block_instance` is null on REST (`modify_rest_query_for_preset`): the editor preview queries the collection endpoint and never renders the block, so there is no instance to carry. The key is set explicitly in both paths, so the context has one shape. +`post_id` comes from `$block->context['postId']` where the block has it, falling back to `get_the_ID()`. The block context is what core resolved for that block; `get_the_ID()` is global loop state, and the two can diverge. + ## Key Files - `hm-query-loop.php` - Main plugin file with all PHP hooks and query modification logic diff --git a/README.md b/README.md index 1af38f1..96f3aa2 100644 --- a/README.md +++ b/README.md @@ -147,7 +147,8 @@ add_action( 'init', function() { 'Related Articles', // Label shown in dropdown function( $query_vars, $context ) { // $context includes: - // - post_id: The post the block is rendering for + // - post_id: The post the block is rendering for ($block->context['postId'], + // falling back to get_the_ID()) // - is_rest: Boolean, true when called from REST API (editor) // - block_instance: The WP_Block being rendered, or null on REST // - block: Array with perPage and page values. Despite the name this is diff --git a/inc/query-presets.php b/inc/query-presets.php index 745aca0..200d676 100644 --- a/inc/query-presets.php +++ b/inc/query-presets.php @@ -220,7 +220,10 @@ function filter_query_loop_block_query_vars( array $query_vars, WP_Block $block, } $context = [ - 'post_id' => get_the_ID() ?? 0, + // Prefer what core resolved for this block over global loop state: the two + // agree on a singular template, and diverge where the global post is not + // what the block is rendering for. + 'post_id' => $block_context['postId'] ?? get_the_ID() ?? 0, 'is_rest' => false, // Presets need the block itself to reach core's context — termId and taxonomy // inside a Term Template, postId, and so on. 'block' below is not that. diff --git a/tests/php/query-presets-test.php b/tests/php/query-presets-test.php index e015e72..c1c75cf 100644 --- a/tests/php/query-presets-test.php +++ b/tests/php/query-presets-test.php @@ -157,6 +157,23 @@ function ( array $query_vars, array $context ): array { check( 'frontend: context carries the block instance', $frontend['block_instance'], $block ); check( 'frontend: is_rest false', $frontend['is_rest'], false ); + // post_id follows the block, not the global loop. $block carries no postId, so + // this one falls back; the next block supplies one that disagrees with the global. + $GLOBALS['current_post_id'] = 12; + filter_query_loop_block_query_vars( [ 'posts_per_page' => 5 ], $block, 1 ); + check( 'post_id: falls back to the global post', $seen_context['records_context']['post_id'], 12 ); + + $other_post_block = new WP_Block( + 'core/post-template', + [ + 'query' => [ 'hmPreset' => 'records_context' ], + 'postId' => 34, + ] + ); + filter_query_loop_block_query_vars( [ 'posts_per_page' => 5 ], $other_post_block, 1 ); + check( 'post_id: block context wins over the global post', $seen_context['records_context']['post_id'], 34 ); + $GLOBALS['current_post_id'] = 0; + // 'block' is misnamed but load-bearing: renaming it would break existing presets. check( 'frontend: block stays pagination metadata', $frontend['block'], [ 'perPage' => 5, 'page' => 2 ] ); From 57276721792667bbfedb307dda7e7be571f25cd9 Mon Sep 17 00:00:00 2001 From: Matthew Haines-Young Date: Tue, 6 Oct 2026 17:36:27 +0100 Subject: [PATCH 3/4] Presets: tighten the preset docs Prose only; no executable line changes. CLAUDE.md describes how the plugin works now, so it cannot lean on a before and after. Drop "already holds" and the sentence about the signature being unchanged: backward compatibility is a fact about a change, not about the plugin, and a reader arriving later has nothing to compare it to. "Code for that rather than expecting a block" told nobody what to do. Say the actual instruction: check block_instance, return $query_vars unchanged when it is null. "isset dance" is a figure of speech. Say what it means. Likewise "front end" as a noun, and label the Term Template snippet as illustrative so nobody goes looking for a preset the plugin does not ship. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 4 ++-- README.md | 10 ++++++---- inc/query-presets.php | 5 +++-- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 0e38e6b..83a26be 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -115,10 +115,10 @@ The plugin provides a PHP API for registering custom query presets that can be s 1. Presets are registered via PHP callbacks that receive query args and context 2. The preset selector appears in the block editor when presets are registered 3. REST API hooks are automatically added for all public post types via `rest_{$post_type}_collection_params` and `rest_{$post_type}_query` -4. Frontend queries are modified via `query_loop_block_query_vars` filter, which already holds the `WP_Block` and puts it in the context as `block_instance` +4. Frontend queries are modified via `query_loop_block_query_vars` filter, which puts the `WP_Block` into the context as `block_instance` 5. The selected preset is stored in `query.hmPreset` block attribute -The callback signature is unchanged — the block travels in the context array that was already there. `block_instance` is null on REST (`modify_rest_query_for_preset`): the editor preview queries the collection endpoint and never renders the block, so there is no instance to carry. The key is set explicitly in both paths, so the context has one shape. +`block_instance` is null on REST (`modify_rest_query_for_preset`): the editor preview queries the collection endpoint and never renders the block, so there is no instance to carry. Both paths set the key explicitly, so the context has one shape. `post_id` comes from `$block->context['postId']` where the block has it, falling back to `get_the_ID()`. The block context is what core resolved for that block; `get_the_ID()` is global loop state, and the two can diverge. diff --git a/README.md b/README.md index 96f3aa2..a5c055e 100644 --- a/README.md +++ b/README.md @@ -171,7 +171,8 @@ add_action( 'init', function() { `$context['block_instance']` is the `WP_Block` being rendered, so a preset can read anything core puts in its context — `postId`, or the `termId` and `taxonomy` a Term -Template provides to the blocks inside it: +Template provides to the blocks inside it. This example is illustrative — the +plugin ships no presets of its own: ```php \HM\QueryLoop\QueryPresets\register_query_preset( @@ -202,9 +203,10 @@ reading it, so both keys stay. `block_instance` is null on REST requests. The editor preview fetches posts from the collection endpoint rather than rendering the block, so there is no block instance to -carry — a preset that depends on block context will behave differently in the editor -preview to the frontend. Code for that rather than expecting a block. The key is -always present, so `?->` is enough and no `isset()` is needed. +carry. A preset that depends on block context behaves differently in the editor +preview from the front end. Check `block_instance` before using it, and return +`$query_vars` unchanged when it is null. The key is always present, so `?->` reads it +safely and no `isset()` check is needed. The callback signature is unchanged, so every existing preset keeps working. diff --git a/inc/query-presets.php b/inc/query-presets.php index 200d676..f4d51b7 100644 --- a/inc/query-presets.php +++ b/inc/query-presets.php @@ -191,8 +191,9 @@ function modify_rest_query_for_preset( array $args, \WP_REST_Request $request ): $context = [ 'post_id' => $request->get_param( 'post_id' ) ?? get_the_ID() ?? 0, 'is_rest' => true, - // Set explicitly, so the context has one shape and presets need no isset dance. - // REST renders no block: the editor preview queries the collection endpoint. + // Set explicitly, so the context has one shape and presets can read it with + // `?->` and no isset() check. REST renders no block: the editor preview + // queries the collection endpoint. 'block_instance' => null, 'block' => [ 'perPage' => $request->get_param( 'per_page' ), From 9f99e053b1324c3651f24349b76f38f3c1312d73 Mon Sep 17 00:00:00 2001 From: Matthew Haines-Young Date: Tue, 6 Oct 2026 18:55:57 +0100 Subject: [PATCH 4/4] Presets: drop the last change-relative line from the docs The README still told the reader the callback signature was unchanged. That is a fact about a pull request, not about the plugin, and whoever opens the README later has no prior version to hold it against. Deleted rather than reworded: the new key lives inside the $context array, so there is no parameter for a callback to decline, and saying so would be noise. Split the CLAUDE.md REST line, which was a colon joining three clauses, into one idea per sentence. The reference to modify_rest_query_for_preset() moves into its own sentence rather than being carried parenthetically. Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 2 +- README.md | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 83a26be..34c8dbc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -118,7 +118,7 @@ The plugin provides a PHP API for registering custom query presets that can be s 4. Frontend queries are modified via `query_loop_block_query_vars` filter, which puts the `WP_Block` into the context as `block_instance` 5. The selected preset is stored in `query.hmPreset` block attribute -`block_instance` is null on REST (`modify_rest_query_for_preset`): the editor preview queries the collection endpoint and never renders the block, so there is no instance to carry. Both paths set the key explicitly, so the context has one shape. +`block_instance` is null on REST. The editor preview queries the collection endpoint and never renders a block, so there is no instance to carry. See `modify_rest_query_for_preset()`. Both paths set the key explicitly, so the context has one shape. `post_id` comes from `$block->context['postId']` where the block has it, falling back to `get_the_ID()`. The block context is what core resolved for that block; `get_the_ID()` is global loop state, and the two can diverge. diff --git a/README.md b/README.md index a5c055e..6c876f2 100644 --- a/README.md +++ b/README.md @@ -208,8 +208,6 @@ preview from the front end. Check `block_instance` before using it, and return `$query_vars` unchanged when it is null. The key is always present, so `?->` reads it safely and no `isset()` check is needed. -The callback signature is unchanged, so every existing preset keeps working. - **Available Functions:** - `\HM\QueryLoop\QueryPresets\register_query_preset( $name, $label, $callback )` - Register a preset