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
- Clean image paths in
getWidth()andgetHeight(): Strip query strings/fragments and decode URL entities soImageFactorycan inspect the file on disk. - Support root-relative public file paths in
toSrcSet(): Resolve root-relative paths viaresolvePath()so standard file paths (/sites/default/files/...) generate responsivesrcsettemplates just like stream wrappers. - Preserve
%20intoSrcSet(): Keep spaces encoded in candidate URLs to comply with WHATWG HTML §4.8.4.3.7. - Preserve decorative alt text: Change
{% if alt %}to{% if alt is defined and alt is not null %}inimage.twig. - 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()andgetHeight() - Support root-relative public file paths in
toSrcSet() - Preserve URL-encoded spaces in
toSrcSet() - Preserve decorative
alt=""incomponents/image/image.twig - Add Unit and Kernel test coverage
- Community review & merge