Overview
Provide an SDC component to render a responsive image that can be included by other components. It should not be visible as an available component in the editor, but should be usable by component authors in their own SDCs. To demonstrate this, additionally provide a basic SDC card component example in one of the testing modules.
Proposed resolution
Create an Image SDC that is hidden from the UI, that can be used as such:
<article class="{{ class ?? 'card' }}">
<header>
<h2>{{ heading }}</h2>
</header>
{% include 'experience_builder:image' with image|merge({
loading,
sizes,
class: 'card--image',
attributes: create_attribute({
'data-testid': 'card-component-image',
})
}|filter((value) => value is not null)) only %}
<div class="content">
<p>{{ content }}</p>
</div>
<footer>{{ footer }}</footer>
</article>
The component is hidden from the UI by adding `noUI: true` to it's component.yml file
$schema: https://git.drupalcode.org/project/drupal/-/raw/HEAD/core/assets/schemas/v1/metadata.schema.json
name: Image
noUi: true
props:
.
.
Screenshots


| Comment | File | Size | Author |
|---|---|---|---|
| #42 | Screenshot from 2025-07-29 16-51-15.png | 1.24 MB | justafish |
| #41 | Screenshot from 2025-07-29 16-45-25.png | 939.42 KB | justafish |
Issue fork experience_builder-3535453
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 #2
justafishComment #3
effulgentsia commented@lauriii and I discussed this and decided to make it a beta blocker. Every design system includes components with images, so providing the XB recommended way to do that is important to include in the beta.
Comment #4
penyaskitoThis is (slightly) related to #3513563: [later phase] [META] Component slot restrictions ("which?") + limits ("how many?") per discussion with @lauriii in slack. We want to restrict an SDC on the root canvas instead of a concrete slot, but the mechanism could be similar or even the same API. For now it's fine to hard-code it.
Comment #5
lauriiiI actually don't think this is exactly the same as what's being worked on there. What we want to do here is prevent exposing the component to the UI at all. Something along the lines of `hide_from_ui: true` property in the SDC schema. The difference is that this is the component indicating it should be hidden from the UI, vs #3513563: [later phase] [META] Component slot restrictions ("which?") + limits ("how many?") where it would most likely be the canvas indicating what it can accept.
Comment #6
effulgentsia commentedField types and Views plugins can have a
no_uikey in their plugin definition, so we can continue with that pattern here, just in the SDC YAML instead of in a PHP attribute.Comment #7
effulgentsia commentedProbably worth opening a core issue to propose adding the
no_uikey to SDC definitions. We don't need to hold this issue up on that actually landing in core, but it would be good to get some initial feedback on it from SDC system maintainers.Comment #8
lauriiiComment #9
penyaskitoI'll work on the backend infra.
Comment #10
penyaskitoI see two alternatives:
ComponentMetadataRequirementsChecker::checkfornoUI == TRUEand don't create the component config entity. Profit.Componentconfig entityI'm tempted for 2, even if potentially more complex, because:
Comment #12
penyaskitoBefore moving further would love thoughts on #10 + the MR direction.
Comment #13
penyaskitoComment #14
penyaskitore: #10: Another reason for #2. When/if we migrate from LB, we might want people to use/edit pages with layouts, but we might not want people to add layouts to new pages. Those would be no_ui components.
Comment #15
larowlanLooks good to me
Comment #16
penyaskitoComment #17
wim leers#10:
This is already possible: the
Componentconfig entity'sstatusneeds to change toFALSE.What is the purpose of this? 🤔 A
Componentconfig entity that keeps getting updated but is not available for anybody to instantiate … what value is there in/problem does it solve to then have a log of all changes that were made to that component?This is definitely true! And that is where the prior point becomes valuable too, I suspect? 🤔
I am still not convinced we need #10.2 aka . For the dependency reason above, the first option in #10 is also not good.
Unless I'm missing something, a hybrid actually makes more sense:
ComponentMetadataRequirementsChecker::checkforno_ui == TRUEanddon'tDO create the component config entity.Profit.But setstatus = FALSE.Add no_ui toComponentconfig entityThat means zero new infrastructure, zero config schema changes, but it achieves everything outlined in #10? 🤓🤞
Comment #18
penyaskitoMaybe I'm wrong, but that would not render the component on existing pages where it already existed, right? As a content manager, I'd like to be able to not affect existing pages, but still block editors from adding them again.
See also #14 where this might be even more relevant.
For the record, I don't know if/where we are using this but we are already preventing components to be added (config is not created) when their category is "Elements".
Comment #19
wim leersYou're wrong :)
Component'sstatusonly controls whether those components are available for content creators to instantiate. Disabling aComponentdoes not and MUST NOT prevent existing instances from working. In fact, that is exactly whyComponentconfig entities were introduced: to use (config) dependency management to guarantee that all existing component trees will continue to work (unless of course you're going to start hacking code and/or bypassing validation).See the original issue that introduced the
Componentconfig entity for more detail if you like: #3444417: "Developer-created components": mark which SDCs should be exposed in XB.EDIT: The docs at
docs/config-management.mdcover this — I should've done that rather than typing the above equivalent from memory 😅:Comment #20
penyaskito#19 Sure, there's no way we could manually disable SDCs as we can now if what you said wasn't true 🤦🏽♂️
Repurposed the MR with that info. Sadly the SDC metadata noUi attribute depends on the core change, we cannot really leap ahead unless we decided to re-parse the metadata (which we don't want).
Comment #21
penyaskitoThis should be ready. We cannot leap ahead of core though. So they will be visible before #3535958: Allow SDCs to be marked to be excluded from UI is released though.
Comment #22
penyaskitoAs I wasn't explicit and I see now we didn't discuss this: I'd expect we land !1307, and a different MR for the actual component in this very same issue.
Comment #23
penyaskitoI asked @larowlan if there's any way the core MR could be backported to 11.2.x, and he clarified that as a new feature it's a no-go.
Per previous conversations, we should just hardcode the component name for now until we have a release and can require 11.3.x.
I didn't do this yet as I don't know the name of the component (image? responsive_image?)
Comment #29
wim leersTIL #3535958: Allow SDCs to be marked to be excluded from UI happened!
RTBC'd the "pure infra" MR https://git.drupalcode.org/project/experience_builder/-/merge_requests/1307 — not the other MR.
Needs follow-up issue to remove the work-around once XB requires >=11.3.
Back to and to Sally for the other MR on this issue :) Thanks, @penyaskito!
Comment #30
penyaskitoCreated #3537695: [11.3.0-and-up] Remove hardcoded Image SDC from `\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\SingleDirectoryComponent::componentMeetsRequirements`
Comment #31
wim leersDid an initial review of the other MR, and raised 2 major concerns 😅
Comment #32
wim leersAlso, the issue summary is very outdated, because it says
and I believe that the thing that concerns me most is something worth calling out in the issue summary.
#3532718: Improve the front-end DX of `<img srcset>` landed 9 days ago.
Comment #33
wim leers@justafish and I met about this MR — we both agrees that https://git.drupalcode.org/project/experience_builder/-/merge_requests/1... is problematic.
The reason that code is in there, is because @justafish understood from talking to @lauriii and from this example in the issue summary:
… that:
examples.0in the metadata) must ALSO be able to generate asrcsetsrcsetThose 2 bits are why this (in-progress) MR is copying local files (both inside the SDC and outside) to some location in
public://: because otherwise no derivative for it can be generated, since\Drupal\image\Controller\ImageStyleDownloadController::deliver()does NOT support "shipped files" (aka files shipped as part of a module or theme), even though the logic in\Drupal\image\Entity\ImageStyle::buildUri()suggests it does (see theelsebranch).(For debugging: install XB, then navigate to
/sites/default/files/styles/xb_parametrized_width--640//core/tests/fixtures/files/image-1.png.webpto generate a 640px-wide derivative ofcore/tests/fixtures/files/image-1.png. Put a breakpoint in the aforementioned methods and observe: it'll try to usecoreas the scheme.)This is why @justafish resorted to copying shipped files into
public://— because core'sImageStyleDownloadControllerdoes not support generating derivatives for shipped files.But precisely that is the very worrying piece here: a Twig extension writing to the file system as a side effect. 😱
This opens a can of worms:
public://copy. How do we efficiently detect such a change?Fileentity does the deduplicating of identically named files; here we would become responsible. A naming scheme is conceivable (public://sdc-default-images/<extension name>/<SDC name>/<filename>) to avoid that, but it's not great.Conclusion
srcsetfor a shipped default image in an SDC is questionable — it is unlikely to be enormous anyway. @justafish understood @lauriii to want this, but based on the issue summary, I'm not confident that is the case. Doubly so because we don't currently do that in HEAD either and this was jointly agreed in #3515646: Add automated <img srcset> generation as a reasonable trade-off.EDIT: I found the original comment covering this.
\Drupal\experience_builder\PropSource\DefaultRelativeUrlPropSource+\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\SingleDirectoryComponent::rewriteExampleUrl()do in HEAD.ImageStyleDownloadControllerand add support for shipped files.srcsetderivatives" bit work, but in what the issue title says: . So: if @lauriii thinks this should happen despite all of the above concerns, then it'll need to be in a non-beta blocking follow-up.Assigning to @lauriii to get feedback on this; meanwhile @justafish will push ahead with everything else.
Comment #34
lauriiiIt's fine to open a follow-up for generating a srcset for shipped images. Sorry, I should have made it clear that it's critical that shipped images work when using the image component, but we don't have to add the full support for that here. Once we have the follow-up, we can figure out how to prioritize that. In the meanwhile, we should just passthrough those images.
I think we need two follow-up issues where we should cover this for both SDC and code components:
Comment #35
wim leers#34: perfect, that sounds completely sensible 😊 — thanks!
Comment #36
penyaskitoComment #37
isholgueras commentedI've left some comments in the MR
Comment #38
wim leersCan we get some screenshots of uses of the new
noUiSDCexperience_builder:image(both preview + its component instance form) in the issue summary? 🙏While reviewing, found some concerns, most importantly
tests/modules/xb_test_sdc/xb_test_sdc.install.Comment #39
justafishComment #40
wim leersFeedback on the MR + there's still 3 "Needs …" tags. 😅
Comment #41
justafishComment #42
justafishComment #43
justafishComment #44
justafishComment #45
justafishComment #46
justafishComment #47
justafishComment #48
penyaskitoI reviewed and fixed my findings, but would need clarification on the Playwright changes. Otherwise this looks ready to me.
I'm not sure if the follow-up from #34 is still needed, re-tagging just to be cautious. The infra follow-up for core 11.3 exists already at #3537695: [11.3.0-and-up] Remove hardcoded Image SDC from `\Drupal\experience_builder\Plugin\ExperienceBuilder\ComponentSource\SingleDirectoryComponent::componentMeetsRequirements`
Comment #49
wim leersThanks for the improvements here, this MR looks much more solid now! 🤩 🙏
In addition to @penyaskito's questions, I also posted a few to the MR — again about documenting the rationale to ensure maintainability (the commits of the past 24 hours made that a lot better — these are the last bits!).
But there's also one bit of missing test coverage (the Twig extension is updated by this MR but
tests/src/Unit/Twig/XbTwigExtensionFiltersTest.phpdid not gain new test cases)Yes, I think we still need 2 follow-up issues for these 2 things Lauri requested:
Comment #50
justafishhttps://www.drupal.org/project/experience_builder/issues/3538858 created for generating srcsets for remote images
I haven't created a follow up for shipped images as we can already do that 😊
Comment #51
justafishComment #52
wim leersThanks for creating #3538858: `ParametrizedImageStyle`: Generate srcset for remote images from allowed 3rd parties! Posted a few questions there. Doing (final! 🤞) review pass here :)
Comment #53
wim leersNo, we don't support that. We're copying a shipped image *to* a stream wrapper to test stream wrappers — quoting
xb_test_sdc_install():See https://git.drupalcode.org/project/experience_builder/-/merge_requests/1... + https://git.drupalcode.org/project/experience_builder/-/merge_requests/1... they literally show that no
srcsetis generated forsdc.xb_test_sdc.card(shipped image inside SDC) norsdc.xb_test_sdc.card-with-local-image(shipped image outside an SDC) — the test expectation make this very clear. 😊Finished review.
Issue created for something surfaced here but not caused here: #3539038: DX: update `PropShapeToFieldInstanceTest` to not test all SDCs, but only those that meet XB's requirements.
Only fixed some nits, but also found some missing test coverage, and that revealed a bug. @justafish, please confirm that you agree with the changes I made for that — see the 2 commits referenced here: https://git.drupalcode.org/project/experience_builder/-/merge_requests/1...
And thanks for teaching me https://git.drupalcode.org/project/experience_builder/-/merge_requests/1... 😃
Comment #54
justafishWim is correct, we discussed this and though we both remember it working we can't actually reproduce it so have decided it was a Folie à deux 😆
Follow up created here https://www.drupal.org/project/experience_builder/issues/3539062
Comment #55
wim leers🤣
Thanks!
Comment #57
wim leersComment #59
wim leersFYI:
cardrequires a publicly resolvable image URI, butcard-with-stream-wrapper-imagespecifically does NOT accept that, but a stream wrapper URI! See https://git.drupalcode.org/project/experience_builder/-/merge_requests/1....(This went unnoticed because
json-schema-definitions://experience_builder.module/image-uri's regex was too loose.)card-with-stream-wrapper-imagerenders an invalidsrc:https://git.drupalcode.org/project/experience_builder/-/merge_requests/1...
Both of these will be fixed as part of #3530351: Decouple image+video (URI) shape matching from specific image+video file types/extensions.
Comment #60
wim leersExtracted #59 into its own issue because it made the other MR unwieldy: #3543783: `card-with-stream-wrapper-image` test SDC generates an invalid `<img src>`.
Comment #61
wim leersAnd thanks to #3530351: Decouple image+video (URI) shape matching from specific image+video file types/extensions, the
card-with-stream-wrapper-imageone now works end-to-end — test to prove it: https://git.drupalcode.org/project/experience_builder/-/commit/9048ab846... 🥳