ppl / module-category-filter-cache-warmer

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.

  • Pawel Pliousnin
magento2-module Compatibility: 2.4.7-2.4.9 Code Quality: Warning Tests: N/A Security: Pass proprietary

Ppl_CategoryFilterCacheWarmer

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:

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

# 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)

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.

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.

Notes

  • Listing-action depth was originally configurable (0-3); as of Unreleased it is pinned to 1 (see above).
  • Does not delete or invalidate cache itself — Magento/Varnish keep full control via cache tags.
  • Multi-store: guaranteed only for store views with a unique base URL or a store code in the URL.
Versions
Version Stability QA Status Compatibility Released
1.3.0 stable Pass Magento 2.4.7-2.4.9 Details 2026-07-12 21:23:52

Requires 18

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

Compatibility

Each Magento release line is installed on its supported PHP versions, then the module is built (DI compilation + static-content deploy) and its unit and integration suites are run. The matrix shows the lines and PHP versions the module is confirmed to install and run on. Code-quality results further down (phpstan, phpcs, …) are reported separately and never affect compatibility.

Compatibility matrix (Magento × PHP)
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. Static analysis runs once across the whole module; PHPStan re-runs per Magento + PHP version because resolvable symbols differ between releases. These NEVER affect the Compatibility badge. 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.

Static analysis results
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's PHP against a real Magento install at the configured gate level. Re-runs per Magento and PHP version because resolvable symbols differ between releases.

PHPStan results by Magento and PHP version
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 for each applicable Magento and PHP version. A test failure speaks to the module's behaviour, not its compatibility with a Magento line, so it is reported here separately and never reddens the compatibility matrix.

Unit tests

Unit tests results by Magento and PHP version
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

Integration tests results by Magento and PHP version
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

Security checks run directly against the module: an audit of its declared dependencies for known vulnerabilities (composer audit) and a scan of its source for malware and web-shell signatures. Each runs once. A malware detection fails the version outright.

Security results
Tool Status Findings Summary
Composer audit Pass 0
Malware scan Pass 0
License
proprietary
Authors
  • Pawel Pliousnin — Developer

More from Pawel Pliousnin Digital Solutions

View vendor
Make it pay

Turn an existing module into recurring revenue.

If you already maintain a Magento 2 module on GitHub or GitLab, listing it on Packagento takes about five minutes. We mirror your tags, handle distribution signing, and route paid licenses through Stripe Connect, so you can keep shipping the way you already do.