Skip to content

Commit 198b9ff

Browse files
authored
Merge pull request #2 from humanmade/post-context-consumer
Post template count override
2 parents 9568dba + 5493617 commit 198b9ff

File tree

13 files changed

+24164
-22786
lines changed

13 files changed

+24164
-22786
lines changed

.github/workflows/playwright-tests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
if: always()
6363
with:
6464
name: playwright-report
65-
path: playwright-report/
65+
path: test-results/
6666
retention-days: 30
6767
if-no-files-found: ignore
6868

.gitignore

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,9 @@ playwright-report/
3030
playwright/.cache/
3131

3232
/vendor/
33+
34+
# Playwright
35+
/playwright-report/
36+
/blob-report/
37+
/playwright/.cache/
38+
/playwright/.auth/

CLAUDE.md

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Overview
6+
7+
HM Query Loop is a WordPress plugin that extends the core Query Loop block with advanced controls for managing multiple query loops on a single page. It provides three main features: posts per page override for inherited queries, hide on paginated pages, and exclude already displayed posts.
8+
9+
## Development Commands
10+
11+
### Build and Development
12+
- `npm run start` - Start development build with watch mode
13+
- `npm run build` - Create production build (required before testing)
14+
- `npm run lint:js` - Lint JavaScript files
15+
- `npm run lint:css` - Lint CSS/SCSS files
16+
- `npm run format` - Format all files
17+
18+
### Testing
19+
- `npm run wp-env start` - Start WordPress test environment (ports 8888 dev, 8889 tests)
20+
- `npm run test:e2e` - Run Playwright end-to-end tests
21+
- `npm run test:e2e:debug` - Run tests in debug mode
22+
- `npm run test:e2e:watch` - Run tests in watch mode (reruns on changes)
23+
- `npm run wp-env stop` - Stop WordPress environment
24+
25+
**Important**: Always run `npm run build` before running tests, as tests run against built assets.
26+
27+
## Architecture
28+
29+
### Block Extension Approach
30+
The plugin uses WordPress block filters to extend the `core/query` block without creating a custom block variant. This allows it to work with any Query Loop block while preserving core functionality.
31+
32+
### Context System
33+
The plugin exposes a `hm-query-loop/settings` context object from `core/query` to `core/post-template`:
34+
```js
35+
{
36+
perPage: number | undefined, // Custom posts per page value
37+
hideOnPaged: boolean, // Whether to hide on paginated pages
38+
excludeDisplayed: boolean // Whether to exclude displayed posts
39+
}
40+
```
41+
42+
Context is registered in both JavaScript (via `blocks.registerBlockType` filters in src/index.js) and PHP (via `filter_block_metadata` in hm-query-loop.php).
43+
44+
### Dual Query Modification Strategy
45+
46+
The plugin handles two different query scenarios:
47+
48+
**For Inherited Queries** (uses main WP_Query):
49+
- `pre_render_block` - Captures block attributes, checks pagination visibility, re-runs main query with modified args
50+
- Main query is modified before rendering to apply settings
51+
- `render_block` - Returns empty string if block should be hidden on paginated pages
52+
53+
**For Non-Inherited Queries** (custom WP_Query):
54+
- `query_loop_block_query_vars` filter - Passes block attributes into WP_Query vars
55+
- This filter only fires for non-inherited queries, which is why the dual approach is necessary
56+
57+
### Post Tracking
58+
- `the_posts` filter tracks displayed post IDs across all query loops on a page
59+
- Global `$displayed_post_ids` array accumulates IDs from rendered query loops
60+
- Subsequent query loops with `excludeDisplayed` enabled filter out tracked IDs via `post__not_in`
61+
62+
### Editor Preview Synchronization
63+
In src/index.js, a `useEffect` hook syncs the `hmQueryLoop.perPage` attribute to `query.perPage` to reflect the override in the editor preview. The `withPostTemplateStyles` filter adds inline CSS to hide excess posts in the editor beyond the `perPage` limit.
64+
65+
## Key Files
66+
67+
- `hm-query-loop.php` - Main plugin file with all PHP hooks and query modification logic
68+
- `src/index.js` - Block filters for adding inspector controls and editor preview behavior
69+
- `tests/e2e/posts-per-page.spec.js` - E2E tests for posts per page functionality
70+
- `tests/e2e/fixtures.js` - Playwright test fixtures for WordPress admin
71+
72+
## Testing Environment
73+
74+
Tests use `@wordpress/env` with WordPress 6.7.1, configured in `.wp-env.json`. The environment includes TwentyTwentyFour and TwentyTwentyFive themes. Tests run on port 8889 and use Playwright with `@wordpress/e2e-test-utils-playwright`.
75+
76+
## Important Implementation Notes
77+
78+
- The plugin modifies queries without creating database entries or custom post types
79+
- All settings are stored as block attributes in post content
80+
- The `$original_paged` global preserves the original pagination state when blocks override it
81+
- Hidden blocks (via `hideOnPaged`) still track their post IDs for exclusion purposes
82+
- The plugin works with both FSE templates and classic posts/pages

hm-query-loop.php

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,10 +35,10 @@ function init() {
3535
add_filter( 'pre_render_block', __NAMESPACE__ . '\\pre_render_block', 10, 3 );
3636
add_filter( 'render_block', __NAMESPACE__ . '\\render_block', 10, 2 );
3737

38-
// Hook pre_get_posts to modify the query.
38+
// Hook query_loop_block_query_vars to modify the query.
3939
add_filter( 'query_loop_block_query_vars', __NAMESPACE__ . '\\filter_query_loop_block_query_vars', 10, 2 );
4040

41-
// Hook into the_posts to track displayed posts.
41+
// Hook into the_posts to track displayed posts and limit post-template posts.
4242
add_filter( 'the_posts', __NAMESPACE__ . '\\track_displayed_posts', 10, 2 );
4343

4444
// Add contexts to query and post-template block.
@@ -109,6 +109,14 @@ function filter_block_metadata( $metadata ) {
109109
*/
110110
$displayed_post_ids = [];
111111

112+
/**
113+
* Track used post IDs within each query loop block.
114+
* Keyed by block ID to scope posts to each query loop.
115+
*
116+
* @var array
117+
*/
118+
$query_loop_used_posts = [];
119+
112120
/**
113121
* Get displayed post IDs.
114122
*
@@ -132,6 +140,31 @@ function add_displayed_post_ids( $post_ids ) {
132140
$displayed_post_ids = array_unique( array_merge( $displayed_post_ids, $post_ids ) );
133141
}
134142

143+
/**
144+
* Get used post IDs for a specific query loop.
145+
*
146+
* @param string $query_id Query loop block ID.
147+
* @return array
148+
*/
149+
function get_query_loop_used_posts( $query_id ) {
150+
global $query_loop_used_posts;
151+
return $query_loop_used_posts[ $query_id ] ?? [];
152+
}
153+
154+
/**
155+
* Add used post IDs for a specific query loop.
156+
*
157+
* @param string $query_id Query loop block ID.
158+
* @param array $post_ids Post IDs to add.
159+
*/
160+
function add_query_loop_used_posts( $query_id, $post_ids ) {
161+
global $query_loop_used_posts;
162+
if ( ! isset( $query_loop_used_posts[ $query_id ] ) ) {
163+
$query_loop_used_posts[ $query_id ] = [];
164+
}
165+
$query_loop_used_posts[ $query_id ] = array_unique( array_merge( $query_loop_used_posts[ $query_id ], $post_ids ) );
166+
}
167+
135168
/**
136169
* Pre-render block callback.
137170
*
@@ -178,7 +211,6 @@ function pre_render_block( $pre_render, $parsed_block ) {
178211
* @return string Block content.
179212
*/
180213
function render_block( $block_content, $block ) {
181-
// Only process core/query blocks.
182214
if ( 'core/query' !== $block['blockName'] ) {
183215
return $block_content;
184216
}
@@ -204,6 +236,14 @@ function render_block( $block_content, $block ) {
204236
* @return array The modified query vars.
205237
*/
206238
function filter_query_loop_block_query_vars( $query, WP_Block $block ) {
239+
if ( $block->name === 'core/post-template' ) {
240+
$attrs = $block->parsed_block['attrs'];
241+
if ( empty( $attrs['hmQueryLoop']['perPage'] ) ) {
242+
return $query;
243+
}
244+
$attrs['hmQueryLoop']['excludeDisplayedForCurrentLoop'] = $block->context['queryId'];
245+
return modify_query_from_block_attrs( $query, $attrs );
246+
}
207247
return modify_query_from_block_attrs( $query, $block->context );
208248
}
209249

@@ -248,6 +288,19 @@ function modify_query_from_block_attrs( $query = [], $attrs = [] ) {
248288
}
249289
}
250290

291+
// Exclude already displayed posts for this loop if enabled.
292+
if ( isset( $settings['excludeDisplayedForCurrentLoop'] ) ) {
293+
$query['query_id'] = $settings['excludeDisplayedForCurrentLoop'];
294+
$displayed_ids = get_query_loop_used_posts( $settings['excludeDisplayedForCurrentLoop'] );
295+
if ( ! empty( $displayed_ids ) ) {
296+
$existing_exclusions = $query['post__not_in'] ?? [];
297+
if ( ! is_array( $existing_exclusions ) ) {
298+
$existing_exclusions = [];
299+
}
300+
$query['post__not_in'] = array_unique( array_merge( $existing_exclusions, $displayed_ids ) );
301+
}
302+
}
303+
251304
return $query;
252305
}
253306

@@ -267,6 +320,7 @@ function track_displayed_posts( $posts, $query ) {
267320
if ( ! empty( $posts ) ) {
268321
$post_ids = wp_list_pluck( $posts, 'ID' );
269322
add_displayed_post_ids( $post_ids );
323+
add_query_loop_used_posts( $query->get( 'query_id', -1 ), $post_ids );
270324
}
271325

272326
return $posts;

0 commit comments

Comments
 (0)