Image SDC fails to generate srcset for URLs with query string and breaks WHATWG syntax for filenames with spaces

Problem/Motivation

When using Canvas's image SDC (components/image/image.twig), real images fail to generate responsive srcset attributes or produce invalid WHATWG syntax, and decorative alt attributes are stripped:

1. URLs with query strings lose their srcset, width, and height

When Canvas renders an image field, it attaches ?alternateWidths=... to the image URL via ImageItemOverride. In components/image/image.twig:

{% set width = width ?? src|getWidth %}
{% set height = height ?? src|getHeight %}
{% set srcset = src|toSrcSet(width) %}

getWidth() passes the raw URL to PHP's ImageFactory. Because the URL contains a query parameter, PHP looks for a file named hero.jpg?alternateWidths=... on disk, which does not exist. getWidth() returns NULL. Then toSrcSet($src, NULL) aborts because it has no width to work with.

  • Before:
<!-- getWidth() failed to find the file due to "?alternateWidths=...", returning NULL. -->
<!-- No srcset, no width/height: browser downloads uncompressed original and layout shifts (CLS)! -->
<img src="/sites/default/files/hero.jpg?alternateWidths=..." class="image" loading="lazy">
  • After:
<img src="/sites/default/files/hero.jpg"
     srcset="/sites/default/files/styles/canvas_parametrized_width--828/public/hero.jpg.avif 828w,
             /sites/default/files/styles/canvas_parametrized_width--1024/public/hero.jpg.avif 1024w"
     width="1920" height="1080" class="image" loading="lazy">

2. Root-relative public file paths without query string lose their srcset

When a template passes a root-relative public file path (e.g. /sites/default/files/hero.jpg from file_url()), toSrcSet() previously only checked isValidUri($src). Since /sites/default/files/hero.jpg is a relative path rather than a URI scheme (public://), it returned NULL and produced no srcset.

  • Before:
<!-- toSrcSet() only checked isValidUri(), returning NULL for root-relative paths! -->
<img src="/sites/default/files/hero.jpg" width="1920" height="1080" class="image" loading="lazy">
  • After:
<img src="/sites/default/files/hero.jpg"
     srcset="/sites/default/files/styles/canvas_parametrized_width--828/public/hero.jpg.avif 828w,
             /sites/default/files/styles/canvas_parametrized_width--1024/public/hero.jpg.avif 1024w"
     width="1920" height="1080" class="image" loading="lazy">

3. Filenames with spaces break WHATWG srcset syntax

In toSrcSet(), urldecode($params[ImageItemOverride::ALT_WIDTHS_QUERY_PARAM]) decodes %20 into literal spaces. According to WHATWG HTML §4.8.4.3.7, whitespace is the syntactic delimiter between the candidate URL and the width descriptor. A literal space inside the URL candidate causes browsers to discard the candidate.

  • Before:
<!-- Invalid WHATWG syntax: whitespace in filename breaks srcset candidate parsing! -->
<img src="/sites/default/files/faculty photo.jpg"
     srcset="/sites/default/files/styles/canvas_parametrized_width--828/public/faculty photo.jpg.avif 828w,
             /sites/default/files/styles/canvas_parametrized_width--1024/public/faculty photo.jpg.avif 1024w"
     width="1920" height="1080" class="image" loading="lazy">
  • After:
<!-- Valid WHATWG syntax: spaces in candidate URLs are escaped as %20 -->
<img src="/sites/default/files/faculty%20photo.jpg"
     srcset="/sites/default/files/styles/canvas_parametrized_width--828/public/faculty%20photo.jpg.avif 828w,
             /sites/default/files/styles/canvas_parametrized_width--1024/public/faculty%20photo.jpg.avif 1024w"
     width="1920" height="1080" class="image" loading="lazy">

4. Decorative empty alt text (alt="") is stripped

In image.twig, {% if alt %} evaluates to false when alt="", omitting the attribute entirely instead of preserving accessibility for decorative images.

  • Before:
<!-- {% if alt %} evaluates to false: alt attribute is stripped completely! -->
<img src="/sites/default/files/hero.jpg" ...>
  • After:
<!-- Preserves accessibility: decorative images keep empty alt attribute -->
<img src="/sites/default/files/hero.jpg" alt="" ...>

Proposed Resolution

  1. Clean image paths in getWidth() and getHeight(): Strip query strings/fragments and decode URL entities so ImageFactory can inspect the file on disk.
  2. Support root-relative public file paths in toSrcSet(): Resolve root-relative paths via resolvePath() so standard file paths (/sites/default/files/...) generate responsive srcset templates just like stream wrappers.
  3. Preserve %20 in toSrcSet(): Keep spaces encoded in candidate URLs to comply with WHATWG HTML §4.8.4.3.7.
  4. Preserve decorative alt text: Change {% if alt %} to {% if alt is defined and alt is not null %} in image.twig.
  5. Automated tests: Provide unit and kernel tests covering query strings, root-relative file paths, filenames with spaces and UTF-8 characters, and decorative alt attributes.

Remaining Tasks

  • Clean paths in getWidth() and getHeight()
  • Support root-relative public file paths in toSrcSet()
  • Preserve URL-encoded spaces in toSrcSet()
  • Preserve decorative alt="" in components/image/image.twig
  • Add Unit and Kernel test coverage
  • Community review & merge
Edited by Lionel Enkaoua