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
keysalways set and the page TTL asmax-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 forcacheto cast it.Cache::mergeMaxAges()compares with===, so a string'-1'is never recognised as permanent.
Steps to reproduce
- Standard install, Blazy 3.0.19 or 3.0.x-dev,
cache.page.max_ageleft at its default of 0,dynamic_page_cacheenabled. - Add an image field to a content type, set its formatter to Blazy, leave the Cache option empty, create a node with an image.
- Enable the cacheability debug headers (
http.response.debug_cacheability_headers: true) and request the node twice as an anonymous user. - Observe
X-Drupal-Cache-Max-Age: 0 (Uncacheable)andX-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-agenorkeys, 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-agetogether with thekeys, cast toint. - Never read
system.performance:cache.page.max_agehere.
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
cacheto theblazy_baseconfig 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
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
Comment #3
introfini commentedMerge 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;
BlazyManagerTestandBlazyFormatterTeststill pass locally on Drupal 10.6 and PHP 8.3.