# ppl/module-category-filter-cache-warmer

> Standalone full-page-cache warmer for Magento 2: crawls category pages and configurable first product-list actions, including filters, sorting, page size, view mode and pagination, so shoppers reach warm built-in FPC or Varnish entries from the first clicks. Supports native Magento query-parameter navigation and PPL Discovery Filters without requiring another PPL module.

`composer require ppl/module-category-filter-cache-warmer`

Canonical URL: https://packagento.com/ppl/module-category-filter-cache-warmer

## At a glance

- **Vendor**: Pawel Pliousnin Digital Solutions (https://packagento.com/ppl.md)
- **Latest version**: 1.3.0 — released 2026-07-12
- **Pricing**: $149.00/year, subscription
- **Package type**: Magento 2 module
- **Status**: active, accepting new buyers

## Installation

This is a paid package. The Packagento install flow is licence-gated, so a Composer install needs a licence + project + project credentials in place first.

1. **Sign in or create an account** at https://packagento.com/customer/account/.

2. **Purchase the package.** Open https://packagento.com/ppl/module-category-filter-cache-warmer, add it to your cart, and complete checkout. A licence is minted automatically once payment clears.

3. **Create or pick a project, then activate the licence on it.**
   - Projects represent the Magento installs you deploy to. Manage them at https://packagento.com/projects/.
   - Activate the new licence on the project you'll deploy this package to. Activation is what generates the Composer credentials scoped to that project.

4. **Add the project credentials to your Magento codebase.**

   Grab the project's public + private key from https://packagento.com/projects/ (open the project, then its Credentials tab), and add them to `auth.json`:

   ```json
   {
     "http-basic": {
       "packagento.com": {
         "username": "ppk_live_...",
         "password": "psk_live_..."
       }
     }
   }
   ```

   Add the Packagento Composer repository to `composer.json`:

   ```json
   {
     "repositories": [
       { "type": "composer", "url": "https://packagento.com" }
     ]
   }
   ```

5. **Install and apply.**

   ```bash
   composer require ppl/module-category-filter-cache-warmer:*
   bin/magento setup:upgrade
   bin/magento setup:di:compile
   bin/magento cache:flush
   ```

## What it does

Standalone full-page-cache warmer for Magento 2: crawls category pages and configurable first product-list actions, including filters, sorting, page size, view mode and pagination, so shoppers reach warm built-in FPC or Varnish entries from the first clicks. Supports native Magento query-parameter navigation and PPL Discovery Filters without requiring another PPL module.

## README

Standalone full-page-cache warmer for Magento 2 / Adobe Commerce. It crawls category pages and, optionally, the
first product-list action levels per store. Filters, sorting, direction, page size, view mode and pagination are
normal guest `GET` requests, so Magento's own Full Page Cache (built-in or Varnish) can be populated *before*
shoppers arrive — the first clicks are then cache HITs instead of cold renders.

It does **not** touch Magento's cache internals: no FPC identifier override, no VCL changes, no cache
plugins/observers, no purges. It only fetches normal storefront URLs; Magento/Varnish keep full control over
cache keys, `X-Magento-Vary` and tag-based invalidation. No dependency on other PPL modules.

### Requirements

- Magento Open Source / Adobe Commerce 2.4.7–2.4.9
- PHP per your Magento version's system requirements (2.4.7 → 8.2/8.3, 2.4.8 → 8.3/8.4, 2.4.9 → 8.4/8.5).
  Module code requires PHP `>=8.2`.
- PHP `ext-curl`, `ext-dom`, `ext-libxml`, `ext-mbstring`

### Install

Composer:

```bash
composer require ppl/module-category-filter-cache-warmer
bin/magento module:enable Ppl_CategoryFilterCacheWarmer
bin/magento setup:upgrade
bin/magento setup:di:compile
```

Manual: copy this folder to `app/code/Ppl/CategoryFilterCacheWarmer`, then run the three `bin/magento` commands above.

### Configure

**Stores > Configuration > PPL > Category Filter Cache Warmer** (all store-scoped, disabled by default):

- **General > Enable Warmer** — master switch (default off).
- **Listing Action Depth is fixed at `1`** — category pages plus one product-list action (a filter, sorter,
  direction, page-size, view-mode or pagination change), i.e. "level 0 + level 1". The admin field is hidden.
  Depth 2+ multiplies the URL space far beyond a realistic full-page cache (a full 2-level warm of the sample
  catalog produced ~2,700 URLs and overflowed a 100 MB Varnish roughly 3x, so the earliest-warmed pages were
  LRU-evicted before shoppers arrived). A depth-1 warm of the same catalog is ~160 URLs and fits comfortably.
  To restore configurable depth, revert `Config::getDepth()` and un-hide the field in `system.xml`.
- **Scope > Options Per Action Group / Max URLs Per Category** — breadth caps. The crawler distributes the
  available budget across distinct action signatures instead of allowing the first large filter to consume it.
  `Max URLs Per Category` (default `2000`) counts logical listing states and bounds every category. With PPL
  Discovery Filters, each logical state can produce both a full-page and an AJAX-fragment request, so the
  physical request count can be almost twice the configured logical-state cap.
- **Performance > Parallel Requests / Timeout / Verify TLS** — throttle + TLS (keep verify on in production).
- **Schedule > Run On Schedule / Cron Expression** — optional cron (global schedule; runs every enabled store).

### Run

```bash
## all enabled stores (depth is fixed at 1: category pages + one product-list action)
bin/magento ppl:cfcw:warm

## one store, category pages only (--depth=0); exit non-zero if any URL errored (for CI/deploy)
bin/magento ppl:cfcw:warm --store=1 --depth=0 --fail-on-error
```

Output reports `HIT / MISS / UNCACHEABLE / UNKNOWN / ERROR`. A run holds a lock, so cron and CLI never overlap.

### How it warms listing actions

The crawler parses Magento's rendered layered-navigation links and toolbar controls. It follows same-origin,
same-category links and turns rendered sorter, direction, page-size and view-mode controls into their canonical
Magento query parameters. A breadth-first traversal adds one real shopper action per level while preserving the
current listing state. Marketing/tracking noise is dropped; listing parameters such as
`product_list_order/dir/limit/mode` and `p` are retained.

When PPL Discovery Filters is active, the visible product update uses a cacheable `ppl_df_fragment=1` request.
The warmer detects that mode and fetches both the full URL and its fragment variant. The fragment is a second
physical request for the same logical listing state, not another depth or budget slot.

### Verifying (staging)

```bash
bin/magento cache:flush
bin/magento config:set ppl_category_filter_cache_warmer/general/enabled 1
bin/magento ppl:cfcw:warm --store=1 --depth=0
## then, as a guest:
curl -sI https://shop.example/some-category.html | grep -i x-magento-cache-debug   # HIT
```

### Known limitations

- **Multi-store:** guaranteed only for store views with a unique base URL or a store code in the URL. Several
  store views on the *same* host without a store code may warm the default store.
- **Filters:** native query-parameter layered navigation and PPL Discovery Filters are covered. Other
  SEO/path-based or AJAX-only layered-navigation extensions may need an adapter.
- **Invalidation:** the warmer never deletes cache. Run it *after* deploys / `cache:clean` / reindex — Magento
  invalidates via tags as usual, and the warmer just re-fills afterwards. Do not auto-flush before warming.
- `X-Magento-Cache-Debug` HIT/MISS is only visible when the environment exposes it (Varnish, or built-in FPC in
  developer mode); otherwise results are counted as `UNKNOWN`.

## Changelog

All notable changes to `Ppl_CategoryFilterCacheWarmer` are documented here. This project follows
[Semantic Versioning](https://semver.org/).

### Unreleased

#### Fixed
- Discovery-Filters AJAX-fragment warming now works on the Hyvä build: detection reads the flat
  configuration JSON in `data-ppl-discoveryfilters-init` (Hyvä) in addition to the per-component
  `data-mage-init` JSON (Luma). Before, a Hyvä storefront was never detected, so NO
  `ppl_df_fragment=1` twin URLs were warmed and every real filter click was an FPC MISS even after
  a full warm (the warmer only requested full-page URLs no AJAX shopper ever fetches).
- The PLAIN category URL + `ppl_df_fragment=1` (the client's clear-all / history-navigation request)
  is warmed too — the level-0 exclusion is gone.
- Hyvä's NATIVE layered navigation (without Discovery Filters) is now crawlable: added the
  `filter-option` / `filter-options-content` class tokens Hyvä uses (Luma's plural `filter-options`
  token never matched them), so native filter links are discovered on Hyvä themes.
- E2E-proven on a Hyvä + Discovery Filters store: full cold cache → one `ppl:cfcw:warm` run
  (670 URLs = full pages + fragment twins) → the exact browser click URLs
  (`?ppl_df_fragment=1&ppl_filter[color][0]=…`, clear-all fragment, full page) are all HIT on first
  touch; a second warm pass reports 670 HIT / 0 MISS (no eviction).
- Note: PPL Discovery Filters' own internal warmer (`ppl:discoveryfilters:cache:warm`, cron warm,
  `caching_mode=warmed`, `warm_*` config) was removed from Ppl_DiscoveryFilters/Ppl_DiscoveryFiltersHV —
  this module is the single FPC warmer for native layered navigation AND Discovery Filters.

#### Changed
- Listing-action depth is now pinned to `1` (category pages + one product-list action = "level 0 + level 1")
  and the admin field is hidden. A full 2-level warm of the sample catalog produced ~2,700 URLs and overflowed
  a 100 MB Varnish roughly 3x, so the earliest-warmed pages were LRU-evicted before shoppers arrived; a depth-1
  warm of the same catalog is ~160 URLs and fits comfortably. The CLI `--depth` override still accepts `0`
  (category pages only) but is capped at `1`. Revert `Config::getDepth()` and un-hide the `system.xml` field to
  restore configurable depth once warm scope is sized to cache capacity.

### 1.3.0 - 2026-07-10

First release.

#### Added
- Standalone full-page-cache warmer: crawls category pages and, optionally, the first product-list action levels
  per store and issues guest `GET` requests to pre-populate Magento's native FPC.
- Breadth-first crawler that parses Magento's rendered layered-navigation links and toolbar controls. Each depth
  level represents one filter, sorter, direction, page-size, view-mode or pagination action; listing parameters
  are preserved while marketing/tracking noise is removed.
- PPL Discovery Filters integration that warms the full listing URL and its cacheable `ppl_df_fragment=1`
  response without charging the fragment as another logical depth or budget slot.
- Guest `X-Magento-Vary` bootstrap (per store, secure-aware base URL) so Varnish caches under the guest key.
- `CategoryUrlProvider` filters to active, auto-generated, category-view rewrites (no 404 pre-warming).
- Admin config (section `ppl_category_filter_cache_warmer`, disabled by default), CLI `ppl:cfcw:warm`
  (`--store`, `--depth`, `--fail-on-error`), and an optional cron run.
- Shared lock across CLI and cron so two warm runs can never overlap.
- Reporting counts `HIT / MISS / UNCACHEABLE / UNKNOWN / ERROR`.
- Same-origin enforcement (absolute, root-relative and protocol-relative links), HTTP/HTTPS-only requests,
  off-host redirect protection, and TLS verification on by default (`performance/verify_ssl`).
- Default per-category logical-state cap of `2000`, with depth-2 and deeper budgets distributed by complete
  action signatures so filter and toolbar groups receive a fair share of the top-N budget.

_(Changelog truncated for .md surface. Full history on https://packagento.com/ppl/module-category-filter-cache-warmer.)_

## Recent Versions

| Version | Released |
|---|---|
| 1.3.0 | 2026-07-12 |

## Dependencies

### Require

| Package | Constraint |
|---|---|
| ext-ctype | * |
| ext-curl | * |
| ext-dom | * |
| ext-libxml | * |
| ext-mbstring | * |
| magento/framework | >=103.0.7 <104.0.0 |
| magento/module-backend | >=102.0.0 <103.0.0 |
| magento/module-catalog | >=104.0.0 <105.0.0 |
| magento/module-catalog-url-rewrite | >=100.4.0 <101.0.0 |
| magento/module-config | >=101.2.0 <102.0.0 |
| magento/module-cron | >=100.4.0 <101.0.0 |
| magento/module-eav | >=102.1.0 <103.0.0 |
| magento/module-page-cache | >=100.4.0 <101.0.0 |
| magento/module-store | >=101.1.0 <102.0.0 |
| magento/module-url-rewrite | >=102.0.0 <103.0.0 |
| php | >=8.1 |
| psr/log | ^1.0 \|\| ^2.0 \|\| ^3.0 |
| symfony/console | ^6.4 \|\| ^7.0 |

## Quality

Latest release (1.3.0) passes the Packagento QA pipeline. Verdicts below are per-cell (Magento line × PHP version) for the matrixed tools, and run-once for the static / security tiers.


### Compatibility

Each Magento line is installed on its supported PHP versions, then the module is built (DI compile + static-content deploy). Cells show passed / failed / untested; staircase gaps render as `–`.

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | Pass | Pass | – | – |
| 2.4.8 | – | Pass | Pass | – |
| 2.4.9 | – | – | Pass | Pass |


### Code Quality

Advisory checks against the module's source. Never affect the Compatibility verdict — a phpcs finding can't make a module incompatible.

#### Static Analysis

Coding standards (phpcs), mess detection (phpmd), copy-pasted code (cpd), PHP cross-version compatibility, composer.json validity. Each runs once for the whole module.

| Tool | Status | Findings | Summary |
|---|---|---|---|
| PHPCS | Warning | 8 | 8 warnings (ruleset: Magento2), 6 auto-fixable with phpcbf |
| PHPMD | Warning | 18 | 18 rule violations (UnusedFormalParameter:12, ExcessiveClassComplexity:3, CountInLoopExpression:2, TooManyPublicMethods:1) |
| Cpd | Pass | 0 |  |
| Composer validate | Info | 1 | valid; 1 advisory note (composer validate --strict) |

#### PHPStan

Type-checks the module against a real Magento install. Re-runs per Magento + PHP version because resolvable symbols differ between releases.

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | Pass | Pass | – | – |
| 2.4.8 | – | Pass | Pass | – |
| 2.4.9 | – | – | Pass | Pass |


### Tests

Unit and integration suites run per Magento + PHP cell. Test failures speak to the module's behaviour, not its compatibility with a line, so they're reported here separately.

#### Unit Tests

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | – | – |
| 2.4.8 | – | N/A | N/A | – |
| 2.4.9 | – | – | N/A | N/A |

#### Integration Tests

| Magento | PHP 8.2 | PHP 8.3 | PHP 8.4 | PHP 8.5 |
|---|---|---|---|---|
| 2.4.7 | N/A | N/A | – | – |
| 2.4.8 | – | N/A | N/A | – |
| 2.4.9 | – | – | N/A | N/A |


### Security

Dependency-advisory audit (composer audit) plus a source malware scan. A malware detection fails the version outright.

| Tool | Status | Findings | Summary |
|---|---|---|---|
| Composer audit | Pass | 0 |  |
| Malware scan | Pass | 0 |  |

## Licence and pricing

Subscription — $149.00/year, auto-renews until cancelled. Includes all minor + patch upgrades within the licensed major. One licence per project; cancelling stops auto-renewal but the existing licence keeps working until its end date.

Refundable within 14 days of first purchase via https://packagento.com/account/refunds/.

## Install via Claude Code or any MCP client

The Packagento MCP server can run the licence + project + Composer steps above in one tool call:

```
purchase_and_install_packages(
  composer_names=["ppl/module-category-filter-cache-warmer"],
  project_id="proj_xxx"
)
```

This handles cart, checkout, licence minting, project activation, and writes auth.json credentials. Connect a client with `claude mcp add packagento https://mcp.packagento.com`. Full setup at https://packagento.com/docs/mcp-setup.

## Vendor

Pawel Pliousnin Digital Solutions is a Magento 2 vendor on Packagento. See https://packagento.com/ppl.md for their full catalogue.

