Problem/Motivation
A component that builds no content is still placed in a region, so Layout Builder wraps the layout and region markup around nothing:
<div class="layout layout--onecol">
<div class="layout__region layout__region--content"></div>
</div>
Empty markup on its own is harmless. It stops being harmless once the wrapper is styled: padding, a border, a background or a grid column all occupy space on the page with no content in them.
The most common trigger is FieldBlock::blockAccess(), which forbids access when the field is empty and has no default value.
This happens even though core already knows the block produced nothing. BlockComponentRenderArray::onBuildRender() stops deliberately when the block is inaccessible or built nothing, "we don't output the block render data if there are no render elements found, but we want to capture the cache metadata from the block regardless", leaving the component build empty.
SectionComponent::toRenderArray() then stamps the cacheability onto that empty build, which is what makes it non-empty again:
$output = $event->getBuild(); // []
$event->getCacheableMetadata()->applyTo($output); // ['#cache' => [...]]
Section::toRenderArray() tests plain truthiness, so the component is placed in a region after all:
if ($output = $component->toRenderArray($contexts, $in_preview)) {
$regions[$component->getRegion()][$component->getUuid()] = $output;
}
Core already solves this for theme regions. BlockPageVariant::build() lifts the blocks' access cacheability to the top level of the render array rather than leaving it on each block, and says why: "This is done to prevent issues with empty regions being displayed."
#2817947: 1. Render regions with no children, so empty checks work, which proposed rendering empty regions instead, was closed on exactly that basis: "This was already fixed in a different way by ensuring that the cacheability metadata from the build is added to the top-level."
Layout Builder never received the same treatment. This issue proposes doing for sections what BlockPageVariant already does for theme regions.
Steps to reproduce
- Enable Layout Builder on the default/full view display of a content type.
- In its layout, add a section containing only a field block for an optional field.
- Create a node and leave that field empty.
- View the node: the section renders its layout and region wrappers with nothing inside them.
Proposed resolution
In Section::toRenderArray(), drop any component that built no content, as Element::isEmpty() reports it, while keeping its cacheability on the section, and skip building the layout when no region received content.
A section that produces no content then renders nothing at all, and a region that receives none is skipped by the layout template. A section being previewed keeps its layout, so that blocks can still be placed into its regions.
A section that holds no components at all is left alone for now. It renders for a different reason, since nothing ever went through the cacheability path above, and a section left deliberately empty may well be wanted. That behavior dates to #2927349: Decouple the Layout Builder UI from entities and has never been discussed, so whether it should change too is open.
Note: this is a structural check, so it only covers what core can already tell at build time: the component returned nothing. It does not cover a component that builds something which happens to render to nothing, which is only knowable after rendering, nor content replaced by a placeholder and filled in later. Those need a different mechanism and are out of scope here.
Remaining tasks
- Agree on the behavior change. Two tests assert the current output and are updated in the MR: SectionRenderTest::testToRenderArrayAccessDenied and LayoutSectionTest::testLayoutSectionFormatterAccess.
- Write the change record.
User interface changes
None. The Layout Builder UI keeps rendering every section, so empty regions stay visible and usable while editing.
Introduced terminology
None.
API changes
A region is no longer handed to the layout plugin or its template when all of its components built nothing.
A layout plugin that iterates the regions it received will see one fewer key. Plugins that need to know about every declared region should read them from the plugin definition. The contributed layout plugins surveyed so far are unaffected.
Templates need no change: every layout template in core already guards its regions with an {% if %}.
Data model changes
None.
Release notes snippet
Layout Builder no longer renders a section's layout and region wrappers when every component in that section builds no content, for example a section holding only a field block for a field that is empty.
Issue fork drupal-3615701
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
herved commentedSome numbers from two production sites I maintain:
- On the first, a stock
layout_onecolsection with 3 field blocks renders empty on 234 of 300 published nodes of that type.- On the second, one section renders empty on 280 of 400 nodes sampled, and a second on 172 of the same 400.
I'm aware of #3533588: Add a mechanism to filter out empty theme regions when all blocks inside are empty which targets emptiness that can only be established after rendering.
The proposal here drops empty components and sections before anything is rendered. So it is complementary.
Comment #4
herved commentedComment #5
marcoscanoMostly nits, IMO the two main reasons that move this to NW are:
- the preview issue for empty components
- we need a Change Record draft before RTBC
--
AI disclaimer:
I have used an LLM to help analyzing this issue, whose output I have reviewed and filtered.
Comment #6
herved commentedThanks @marcoscano, suggestions applied and CR drafted (I hope it reads well).
Comment #7
danielvezaThanks for the issue and the work done so far! Could you please see if this issue is a duplicate of #3065418: Empty layout sections get rendered and move your work there if so? That issue has a bit more history and I think it would be good to focus the work in one place
Thanks so much!
Comment #8
herved commented#7: Thanks for mentioning it, I wasn't aware of that issue.
There's a clear overlap, and the goal is emptiness filtering, but I'm not sure if they are the same issue.
#3065418 targets sections the author configured with no components; this one skips components that build nothing at runtime, usually FieldBlock::blockAccess() on an empty optional field.
I deliberately left the first case out of scope for now, to keep the behavior change minimal. From the CR: "A section that has no components at all is unchanged and still renders as before."
Technically, they also target different layers. #3065418 targets LayoutDefault::build(), while this issue targets Section::toRenderArray() that sits upstream of the layout plugin.
So it also covers plugins that build their own array instead of calling parent::build(), it already receives $in_preview and needs no route inspection, and it can return a build that is genuinely Element::isEmpty().
I could close this as a duplicate and move the work over there if others read it as one issue, but I'm worried it's only going to cause confusion.