Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 21 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co

## Overview

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.
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. Core features: posts per page override for inherited queries, hide on paginated pages, exclude already displayed posts, multiple post templates per query loop, query ID deduplication, and query presets.

## Development Commands

Expand All @@ -30,8 +30,9 @@ HM Query Loop is a WordPress plugin that extends the core Query Loop block with
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.

### Context System
The plugin exposes a `hm-query-loop/settings` context object from `core/query` to `core/post-template`:
The plugin exposes an `hmQueryLoop` context object from `core/query` to `core/post-template`:
```js
// context key: 'hmQueryLoop'
{
perPage: number | undefined, // Custom posts per page value
hideOnPaged: boolean, // Whether to hide on paginated pages
Expand Down Expand Up @@ -60,7 +61,18 @@ The plugin handles two different query scenarios:
- Subsequent query loops with `excludeDisplayed` enabled filter out tracked IDs via `post__not_in`

### Editor Preview Synchronization
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.
`withPostTemplateStyles` HOC injects an inline `<style>` tag that hides posts outside each post template's slice in the editor preview using `nth-of-type` selectors. It accounts for both query-level `perPage` (inherited queries) and post-template-level `perPage` (multiple post templates), plus an offset for preceding templates.

### Multiple Post Templates
A non-inherited Query Loop can contain multiple `core/post-template` blocks, each showing a different slice of the results:
- `withPostTemplateInspectorControls` HOC adds "Posts per template" to each `core/post-template`'s inspector, clamped to remaining available posts.
- `withQueryLoopContextProvider` HOC wraps `core/query` with a `UsedPostsContext.Provider` so sibling post-template blocks share their `perPage` values.
- Server-side: `filter_query_loop_block_query_vars` computes `posts_per_page` and offset per template using `$query_loop_post_template_per_pages` (keyed by `queryId`).

### Query ID Deduplication
WordPress does not deduplicate `queryId` when blocks are copy-pasted, breaking post exclusion and pagination:
- Server-side: `deduplicate_query_ids` (`pre_render_block`, priority 10) generates unique IDs using a static instance counter + post ID and propagates them to child `core/post-template` via a dynamic `render_block_context` filter.
- Editor-side: `withUniqueQueryId` HOC computes the expected ID from post ID + block index and syncs it via `setAttributes`.

### Query Presets System

Expand Down Expand Up @@ -92,12 +104,16 @@ The plugin provides a PHP API for registering custom query presets that can be s
- `hm-query-loop.php` - Main plugin file with all PHP hooks and query modification logic
- `inc/query-presets.php` - Query presets registration API and hooks
- `src/index.js` - Block filters for adding inspector controls and editor preview behavior
- `tests/e2e/posts-per-page.spec.js` - E2E tests for posts per page functionality
- `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
- `tests/e2e/multiple-post-templates.spec.js` - E2E tests for multiple post templates
- `tests/e2e/unique-query-id.spec.js` - E2E tests for query ID deduplication
- `tests/e2e/exclude-with-post-in.spec.js` - E2E tests for exclusion with post__in queries

## Testing Environment

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`.
Tests use `@wordpress/env` with WordPress 6.9, configured in `.wp-env.json`. The environment includes TwentyTwentyFour and TwentyTwentyFive themes, and the Advanced Query Loop plugin. Tests run on port 8889 and use Playwright with `@wordpress/e2e-test-utils-playwright`.

## Important Implementation Notes

Expand Down
56 changes: 16 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ A WordPress plugin that extends the core Query Loop block with advanced controls

When a Query Loop block is set to "Inherit query from template", you can now override the number of posts to display. This is useful when you want to show a different number of posts than the main query. Leave the field empty to use the default number of posts.

**Editor Preview**: The posts per page override is now reflected in the editor preview, making it easier to see how your content will appear without needing to preview or publish the page.
**Editor Preview**: The posts per page override is reflected in the editor preview, making it easier to see how your content will appear without needing to preview or publish the page.

### 2. Hide on Paginated Pages

Expand All @@ -20,7 +20,15 @@ Enable this option to automatically exclude posts that have been displayed by pr

**Important:** The exclusion applies to all query loops rendered before the current one, regardless of whether they were visible (e.g., hidden due to pagination settings).

### 4. Query Presets
### 4. Multiple Post Templates

A single Query Loop block (non-inherited) can contain multiple `core/post-template` blocks, each showing a different slice of the query results. Each Post Template block gets a "Posts per template" setting in its inspector controls to control how many posts it shows.

### 5. Query ID Deduplication

The plugin automatically assigns unique query IDs when blocks are copy-pasted or when a page renders the same template multiple times, preventing broken post exclusion and pagination.

### 6. Query Presets

Register custom query configurations in PHP that can be selected from a dropdown in the block editor. This allows developers to create reusable, dynamic queries (like "Related Articles" or "Trending Posts") that content editors can easily apply to any Query Loop block.

Expand Down Expand Up @@ -74,62 +82,30 @@ See [tests/e2e/README.md](tests/e2e/README.md) for more details on the test setu
## Usage

1. Add a Query Loop block to your page or template
2. In the block settings sidebar, find the "HM Query Loop Settings" panel
2. In the block settings sidebar, find the "Extra Query Loop Settings" panel
3. Configure the options as needed:
- **Query Preset**: Select a predefined query configuration (only visible when presets are registered)
- **Posts per page (Override)**: Only visible when inheriting query - enter a number to override posts per page, or leave empty to use default
- **Hide on paginated pages**: Toggle to hide this block on page 2+
- **Exclude already displayed posts**: Toggle to avoid showing duplicate posts
4. For non-inherited queries with multiple Post Template blocks, select each `core/post-template` and set **Posts per template** to control how many posts each template shows

## Block Context

The plugin exposes a single `hm-query-loop/settings` context object that can be accessed by child blocks (like `core/post-template`):
The plugin exposes an `hmQueryLoop` context object from `core/query` to `core/post-template`:

```js
// context key: 'hmQueryLoop'
{
perPage: number | undefined, // Custom posts per page value
hideOnPaged: boolean, // Whether to hide on paginated pages
excludeDisplayed: boolean // Whether to exclude displayed posts
excludeDisplayed: boolean // Whether to exclude displayed posts
}
```

This context is automatically registered for the `core/post-template` block both in JavaScript and PHP.

## Technical Implementation

The plugin uses a dual-approach to handle both inherited and non-inherited Query Loop blocks:

### For Inherited Queries (uses main query):

- **`pre_render_block`**: Runs before a Query Loop block renders
- Captures block attributes and stores them globally
- Checks pagination visibility settings (returns empty string if hidden)
- Hooks `modify_query_from_block_attrs` to `pre_get_posts`

- **`pre_get_posts` (via `modify_query_from_block_attrs`)**: Dynamically hooked during block rendering
- Retrieves block attributes from global storage
- Modifies the query (posts per page, exclusions)
- Works with inherited queries and main query

- **`render_block`**: Runs after the Query Loop block renders
- Unhooks `modify_query_from_block_attrs` from `pre_get_posts`
- Clears global block attributes

### For Non-Inherited Queries (custom WP_Query):

- **`query_loop_block_query_vars`**: Passes custom block attributes into WP_Query vars
- Adds `hm_query_loop_id`, `hm_query_loop_per_page`, `hm_query_loop_exclude_displayed`
- This filter runs for non-inherited queries only

Note: Since `query_loop_block_query_vars` doesn't fire for inherited queries, we use the `pre_render_block`/`render_block` approach to hook/unhook `pre_get_posts` dynamically around the block rendering.

### Post Tracking:

- **`the_posts`**: Runs after posts are fetched
- Tracks post IDs from Query Loop blocks (both approaches) and main query
- Builds a global list for subsequent query loops to exclude

### Query Presets:
## Query Presets API

Register custom query presets that appear in the block editor and modify queries on both frontend and REST API requests.

Expand Down
Loading