Problem/Motivation

\Drupal\blazy\Config\Config::getCacheMetadata() builds the #cache of every Blazy element (and, through it, every Slick and other Blazy-based formatter). When the formatter's Cache option is empty, which is the shipped default (BlazyDefault::baseSettings() sets 'cache' => 0), it falls back to system.performance:cache.page.max_age and stamps that value as the element's max-age:

$max_age   = $this->config('cache.page.max_age', 'system.performance');
$max_age   = empty($settings['cache']) ? $max_age : $settings['cache'];
...
$cache['max-age']  = $max_age;
$cache['keys']     = $blazies->get('cache.metadata.keys', [$id]);

cache.page.max_age is the browser and proxy TTL Drupal sends downstream in Cache-Control. It is not a render lifetime, and Drupal ships it as 0. On that default, every Blazy element declares max-age: 0, the zero bubbles up, and the whole page is rejected by the dynamic page cache (X-Drupal-Dynamic-Cache: UNCACHEABLE (poor cacheability)). Sites that raise the page TTL never notice; sites that keep it at 0 on purpose, for example because their HTML varies by visitor and must not be shared by a CDN, lose the dynamic page cache on every page that renders a Blazy field.

Two smaller things sit on the same line:

  • The option's description says "Leave empty to disable caching", but with keys always set and the page TTL as max-age, an empty option on a site with a positive page TTL render-caches the element anyway.
  • An explicit value arrives from the formatter form as a string ('3600', '-1'), and there is no config schema entry for cache to cast it. Cache::mergeMaxAges() compares with ===, so a string '-1' is never recognised as permanent.

Steps to reproduce

  1. Standard install, Blazy 3.0.19 or 3.0.x-dev, cache.page.max_age left at its default of 0, dynamic_page_cache enabled.
  2. Add an image field to a content type, set its formatter to Blazy, leave the Cache option empty, create a node with an image.
  3. Enable the cacheability debug headers (http.response.debug_cacheability_headers: true) and request the node twice as an anonymous user.
  4. Observe X-Drupal-Cache-Max-Age: 0 (Uncacheable) and X-Drupal-Dynamic-Cache: UNCACHEABLE (poor cacheability) on both requests. Remove the Blazy field, or switch the formatter to the core image formatter, and the same page reports -1 (Permanent) then MISS and HIT.

Measured on a Drupal 10.6 Commerce site with 21 Blazy and Slick formatter instances across 19 view displays: instrumenting CacheableMetadata::createFromRenderArray() named the Blazy image fields as the origin of the zero on the front page, the product listing and the product page, and a login page with no Blazy field reported -1. Setting cache.page.max_age to a positive value made the pages cacheable, which confirms the source but is not an acceptable fix on a site whose HTML must not be shared by a proxy.

Proposed resolution

Treat the formatter Cache option as what its description says it is: a render cache lifetime for this element alone.

  • Empty: declare neither max-age nor keys, so the element inherits whatever its parents and children declare, and is not render-cached on its own.
  • A number, or -1: declare that max-age together with the keys, cast to int.
  • Never read system.performance:cache.page.max_age here.

Merge request against 3.0.x with the change and a kernel test (BlazyCacheMetadataTest) covering empty, unset, empty string, a number, a numeric string, -1 and '-1'. The test fails on the current code with 5 failures (max-age present for the empty cases, '3600' and '-1' not identical to the integers) and passes with the fix; the existing BlazyManagerTest and BlazyFormatterTest still pass.

Remaining tasks

  • Review.
  • Forward-port to 4.0.x, where the same lines exist.
  • Optionally, add cache to the blazy_base config schema as an integer so the form value is cast on save as well.

User interface changes

None. The Cache option keeps its labels and meaning; an empty option now does what its description already promised.

API changes

None. getCacheMetadata() keeps its signature. An empty Cache option no longer returns max-age and keys entries, which is a behaviour change for any site that relied on the page TTL leaking into the element.

AI-Generated: Yes (Claude Code drafted the change, the kernel test and this summary from measurements taken on the site; reviewed and run locally before submitting).

Issue fork blazy-3624950

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

introfini created an issue. See original summary.

introfini’s picture

Status: Active » Needs review

Merge request !34 against 3.0.x, with the change and a kernel test. The test fails on the current code with 5 failures and passes with the fix; BlazyManagerTest and BlazyFormatterTest still pass locally on Drupal 10.6 and PHP 8.3.