Problem/Motivation

For a long time, there has been a pattern in themes to render regions conditionally based on whether the blocks in them have content.

This is most commonly used for sidebars where if they're empty you want them to completely disappear from the page to allow other regions to expand.

As with #953034: [meta] Themes improperly check renderable arrays when determining visibility, using render|striptags|trim|length > 0 is not actually a reliable way to check if a region is empty, due to render placeholders - whether big pipe or not. The placeholders, usually <drupal-render-placeholder> mean there will always be something in the region at the point it gets rendered, which will then be replaced with the actual content, or nothing, later in the request. When the placeholders are replaced with nothing, the region is rendered despite being 'empty'.

Core has gradually been using placeholdering for more and more blocks to take advantage of cache, cache tag, entity, and path alias multiple loading across placeholders. This means that what was previously a fragile pattern that mostly worked, increasingly works less and less.

Proposed resolution

Unless we were able to somehow invert block and region rendering, so that blocks are rendered, including placeholder replacement, before regions, it's not possible to reliably detect an empty region in the region twig template.

This leaves two or three possible approaches:

1. Post process the HTML after regions are rendered in a response filter, this is implemented in https://git.drupalcode.org/project/drupal/-/merge_requests/15685

This works, but it's somewhat expensive (5ms on more or less every request except cached pages, and 1mb of memory in my testing with Umami), and we can't rely on PHP to reproduce exactly the HTML that it is parsing when it outputs from Dom/HtmlDocument so we have to combine dom + string replacement.

Additionally, this approach would not work for js Big Pipe placeholders because those are replaced after the response is sent, we'd have to add a js implementation of the same logic to big_pipe.js to make it work for all cases.

2. Use the CSS :empty pseudo class to identify regions and hide them that way. This is implemented in https://git.drupalcode.org/project/drupal/-/merge_requests/15695

The :empty selector only works if an element is fully empty, as in any whitespace counts as non-empty - the CSS 4 selector spec changes this but isn't implemented in any browsers yet.

However we can adjust the region template so that fully empty regions have no extra whitespace to allow it to be used.

This has zero PHP overhead other than adding a data attribute, the CSS approach will also work for big pipe because when it replaces a big pipe placeholder with an empty string, the selector will match.

The only disadvantage of this approach is it relies on display:none rather than removing the elements from the DOM

3. Use JavaScript. This would be possible to implement, but it would either cause content layout shift if the JavaScript was added in the footer, or it would require js in the header which would be render blocking, we could use inline js but we'd need to make it compatible with content security policies, also it would result in a style recalculation.

4. Find a way to invert things so that block placeholders are still rendered as placeholders but are also rendered before the region template, this would probably require special logic in block module so it would end up block-module specific and not work for contrib page builders.

Remaining tasks

User interface changes

Introduced terminology

API changes

Data model changes

Release notes snippet

Issue fork drupal-3533588

Command icon 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

jonnyhocks created an issue. See original summary.

jonnyhocks’s picture

I've opened a merge request just reverting the update to return false now until a solution is put forward.

quietone’s picture

Version: 11.2.x-dev » 11.x-dev
Issue summary: View changes

If this problem was discovered on a version of Drupal that is not 11.x, add that information in the issue summary and leave the version at 11.x. In Drupal core changes are made on on 11.x (our main development branch) first, and are then back ported as needed according to the Core change policies. Also mentioned on the version section of the list of issue fields documentation. Thanks.

kiwad’s picture

I have this code in twig :

{% set aside_start_top_rendered = page.aside_start_top|render %}
{% set aside_start_bottom_rendered = page.aside_start_bottom|render %}

{% set aside_exists = (aside_start_top_rendered is real_content or aside_start_bottom_rendered is real_content) and not narrow and container_type != 'fluid' %}

After updating from 11.1.18 to 11.2.2, aside_exists now always returns TRUE because of placeholders

Before 11.2.2, my block had this in it :

{"cfa_navigationprincipale":{"#cache":{"keys":["entity_view","block","cfa_navigationprincipale"],"contexts":["languages:language_interface","route.menu_active_trails:main"],"tags":["block_view","config:block.block.cfa_navigationprincipale","config:system.menu.main"],"max-age":-1},"#weight":0,"#lazy_builder":["Drupal\\block\\BlockViewBuilder::lazyBuilder",["cfa_navigationprincipale","full",null]]},"#sorted":true,"#theme_wrappers":["region"],"#region":"aside_start_top"}

After 11.2.2

 {"cfa_navigationprincipale":{"#cache":{"keys":["entity_view","block","cfa_navigationprincipale"],"contexts":["languages:language_interface","route.menu_active_trails:main"],"tags":["block_view","config:block.block.cfa_navigationprincipale","config:system.menu.main"],"max-age":-1},"#weight":0,"#lazy_builder":["Drupal\\block\\BlockViewBuilder::lazyBuilder",["cfa_navigationprincipale","full",null]],"#create_placeholder":true},"#sorted":true,"#theme_wrappers":["region"],"#region":"aside_start_top"}  

As a workaround for now, I did in custom module :

function mymodule_preprocess_page(array &$variables) {
  if (isset($variables['page']['aside_start_top']['cfa_navigationprincipale'])) {
    $variables['page']['aside_start_top']['cfa_navigationprincipale']['#create_placeholder'] = FALSE;
  }
  if (isset($variables['page']['aside_start_bottom']['cfa_navigationprincipale'])) {
    $variables['page']['aside_start_bottom']['cfa_navigationprincipale']['#create_placeholder'] = FALSE;
  }
}
hamza_niazi’s picture

I was facing the same issue but for merge 12587 works fine

jmagunia’s picture

There are also other files this temporary fix needs to be applied to. For example:

core/modules/system/src/Plugin/Block/SystemMenuBlock.php

ac’s picture

Title: Views blocks now use placeholders, making it impossible for the theme to determine if it’s empty or not » Core blocks now use placeholders, making it impossible for the theme to determine if the region is empty
Component: views.module » render system
Priority: Normal » Major

This stems from this commit: https://git.drupalcode.org/project/drupal/-/commit/bc383a3

As far as I can tell this now makes it impossible to determine if a region should be rendered by the theme layer. I would think this is a fairly major issue.

weseze’s picture

StatusFileSize
new3.33 KB

Static patch based on the MR above, for use in composer workflows.

Ran into this issue with a views block that is empty, but still rendering the HTML for it.
MR / patch resolves this issue.

ghost of drupal past’s picture

This is yet another facet (ahem) of #953034: [meta] Themes improperly check renderable arrays when determining visibility

Placeholdering happens for performance reasons but it clashes with theming. It does not even seem possible to fix this but what do I know.

godotislate’s picture

Placeholdering happens for performance reasons but it clashes with theming. It does not even seem possible to fix this but what do I know.

The only thing I can think of is more of a workaround than a solution, and that is to add a configuration option to the affected block plugins that would allow placeholdering to be disabled on a per block basis via the block configuration form. But I don't think it'd be viable long term as the use of placeholdering and/or Fibers/async, etc. expands for better performance during rendering.

jrb’s picture

We've had this issue on a couple sites now. We've fixed this without patching core by adding something like this to our .theme file for the blocks where we're using the page.sidebar_first|render|striptags|trim pattern :

/**
 * Implements hook_preprocess_HOOK().
 */
function example_preprocess_page(&$variables) {
  if (!empty($variables['page']['sidebar_first']['example_mainnavigation']['#create_placeholder'])) {
    $variables['page']['sidebar_first']['example_mainnavigation']['#create_placeholder'] = FALSE;
  }
}

This way, you can turn off the placeholder functionality in only the places where it's causing this problem.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

catch’s picture

It should be possible to filter out empty regions just before the page is sent via post-processing the HTML. It would obviously require some standardised HTML, maybe a data attribute, to identify regions, and then find all regions with that data attribute and remove them if they have no child elements.

We could add a response listener (not sure exactly where but somewhere like that) for HTML pages, maybe have it conditional operate based on something in theme.info.yml - then themes can add that flag when they remove the non-working Twig attempts.

That approach wouldn't work for big pipe, but we could add a js equivalent in big_pipe.js potentially that does the same thing after all placeholders are replaced.

It wouldn't work at all for no-js big pipe placeholder replacement but by default that is only used for things that aren't HTML elements, or in the case where js is disabled, which probably comes under 'graceful degradation'.

With cached placeholder strategy (and placeholder strategy denylists for special cases), a lot less requests actually use big pipe than they used to, so the no-js placeholder case is really minimal now.

catch’s picture

Status: Active » Postponed

Also this should be marked duplicate of #953034: [meta] Themes improperly check renderable arrays when determining visibility, I'm moving it to 'postponed' for now.

catch’s picture

Title: Core blocks now use placeholders, making it impossible for the theme to determine if the region is empty » Add a mechanism to filter out empty theme regions when all blocks inside are empty
Category: Bug report » Feature request
Status: Postponed » Active

Actually that issue is a meta and includes some other scenarios other than blocks.

Re-titling and moving to a feature request, I might try to get a proof of concept of #15 implemented but if someone else wants to try that would be great.

Needs a response listener that gets the request content for HTML responses, gets all elements with a specific data attribute (TBD), and removes them if they have no children, then puts the updated HTML back on the response. We then need to add that data attribute in region.html.twig - can be done in ::preprocessRegion(). And then make the response listener only do its work if the active theme has a .info.yml key set.

There will be PHP overhead involved here but likely to be considerably less than the PHP and database overhead we've saved via placeholdering blocks.

catch’s picture

Status: Active » Needs review

Added an MR. This needs test coverage but I would love manual testing from anyone experiencing the bug - I tested manually with Umami and the help region.

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new839 bytes

The Needs Review Queue Bot tested this issue. It fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

catch’s picture

StatusFileSize
new277.88 KB

Switched to str_replace() for the actual replacements, using $dom->saveHtml() resulted in various encoding/escaping weirdness in tests.

Did some quick profiling of the current status and it's coming in about 4ms.

godotislate’s picture

Since the IS mentions render|striptags|trim|length > 0 as empty content detection, I wonder if there can be an option to allow 'data-filter-empty-region' to have a value of "noemptytags" or something, which could be set in a theme (or module's) preprocess, and in the subscriber, would including stripping the tags from the region content to detect "emptiness".

godotislate’s picture

Re stripping tags: on second thought, there's a risk of removing blocks that only have <img /> tags.

catch’s picture

Handful more changes to try to get tests passing and avoid triggering notices/warnings.

Still about 4ms time taken and about 1mb memory on the front page of Umami logged in. So it's not free, still think it's better than themes manually undoing the placeholder performance improvements we've added, and we can still make it opt-in in theme.info.yml

catch’s picture

Status: Needs work » Needs review
Issue tags: +Needs tests

Re stripping tags: on second thought, there's a risk of removing blocks that only have Only local images are allowed. tags.

Yeah it seems risky and I'm not sure it's necessary. If a block is rendering empty markup that's a problem with the block plugin or configuration usually.

I wondered a bit if we couldn't use https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/:empty for this instead of PHP, select empty regions with CSS and display: none, and the answer is not really because it doesn't trim whitespace. Maybe we could trim the whitespace in Twig then use it, but feels fragile.

Got to a green test run here so while this still needs test coverage, moving to needs review for the concept.

catch’s picture

Added an additional MR here that attempts to achieve the same thing with the CSS :empty pseudo class, it does not work yet because there is one additional bit of whitespace in the region divs that I don't know where it comes from. The advantage of CSS is it would transparently work for javascript big pipe placeholders too (which are replaced or removed after the response is sent).

alexpott made their first commit to this issue’s fork.

catch’s picture

Issue summary: View changes
Issue tags: -Needs tests

Tried to update the issue summary for the two options that have MRs and also two options that I vaguely thought about but mostly rejected as either not as good or too hard.

catch changed the visibility of the branch 3533588-response-filter to hidden.

catch changed the visibility of the branch 3533588-views-blocks-now to hidden.

catch’s picture

Since both me and @alexpott are in favour of the CSS option, I've hidden the other two MRs for now.

alexpott’s picture

I think the CSS option wins because it works for big pipe too which feels really nice.

jwilson3’s picture

Unsure if this is relevant here or not, but I looked into :empty for this kind of thing some time ago, and in addition to whitespace issues mentioned which you can typically fix with {% apply spaceless %} in a custom theme's region templates, (IIRC) it also easily breaks when Twig Debug is on, complicating the theming process entirely.

grimreaper’s picture

Status: Needs review » Needs work
StatusFileSize
new127.66 KB

Hello,

I have tested the MR applying only the changes from:
- core/lib/Drupal/Core/Theme/ThemePreprocess.php
- core/modules/system/css/components/hidden.module.css
- core/modules/system/templates/region.html.twig

See screenshot, even if the component is empty with the data attribute, it is still displayed.

Tested with Brave and Firefox.

seanpb’s picture

Relying on a CSS method (display: none;) to hide unused regions will "break" css psuedoselectors like :nth-child(), :first-child, :last-child. It'd also not work when you'd otherwise want to use immediate sibling selectors - say, if you want to target a main content area immediately preceded by a sidebar with something like .region--left-sidebar + .region--main-content - this would still apply styles to .region--main-content if you used display: none; on the .region--left-sidebar.

For this reason, I don't think the CSS approach ([data-filter-empty-region]:empty { display: none; }) is a good idea.

catch’s picture

@seanpb do you actually do those things in themes? Seems like quite a bit of an edge case.

If we do want to support that, then we could add both approaches, but make the response filter opt-in via theme .info.yml as discussed above. If we can do this with zero PHP overhead for most sites, I think we should try to do that as the default behaviour.

If we put the CSS into its own library, we could then conditionally add it in system_page_attachments() based on whether the theme opts into the response filter approach or not too - although it's only one line of CSS it would be consistent with #2880237: [meta] Refactor system/base library, could maybe do that anyway thinking about it.

catch’s picture

Status: Needs work » Needs review

Discussed with @grimreaper in slack - we were able to figure out why the MR wasn't working with his theme but also went over a couple of other things.

1. There can be use-cases to filter other empty containers, the same mechanism will work for non-regions both with the CSS and response filter approach. I made the data attribute have a more generic name. Other systems wanting to do this like page builders would need to add the data attribute to the relevant container (layout component etc.)

2. To support potentially having a choice of the CSS or response filter approaches, I moved the CSS to its own library in core and attaching it in region preprocess where the data attribute is set. We can make the library attachment depend on theme info.yml if we do end up supporting both ways. Also makes it easier for themes to customise.

grimreaper’s picture

Discussed with @catch, I have added some spaceless into the SDC component I used for the help region in ui_suite_bootstrap, then it is working! Got a little flickering effect due to big_pipe, and being in dev mode without cache.

I think to cover more cases, a deeper solution (server side) will be needed but that's a good start.

Could the PHP filtering be made as a config (potentially hidden) or a setting?
I don't think option in the theme.info.yml is the best place. This feels strange to me to have to ask the front developer to know that and it may depend on the site usage, not the code itself.

PS: comment posted at the same moment :)

catch’s picture

@grimreaper site usage is a good point, it's really the combination of conditional blocks that may or may not leave an entire region empty in addition to a theme that depends on empty regions not appearing at all (which not all do). If you only have one of these you don't necessarily have a problem.

I don't think I'd want to add a UI option for this, not going to be easy to explain, although wouldn't block it if a product or UX manager thinks we should.

We could add a settings.php flag, or we could add a parameter to the service set to FALSE, and then have an example in the site-specific services.yml setting it to TRUE. That last one would be the least amount of code to implement and most self-contained.

godotislate’s picture

Minor aside: Twig has deprecated the spaceless filter for removal in version 4: https://twig.symfony.com/doc/3.x/deprecated.html#filters. (All usages were removed from Drupal core in #3486170: Remove use of deprecated "spaceless" filter in core templates.)

There's been no announcement about a projected Twig 4 release date, but it might be best to prepare other whitespace control implementations in contrib/custom templates and avoid using spaceless in conjunction with the solution that lands here.

catch’s picture

grimreaper’s picture

Also another point about NOT putting in theme.info.yml, what about sub-themes? This smells opening doors to more issues than it is closing.

@catch thanks, I forgot the option of service parameter, which seems very good too.

alexpott’s picture

Also #36 is completely big-pipe incompatible - regardless of the solution here, and any removal of the region by a response event is also going to be ineffective. Basically .region--left-sidebar + .region--main-content is not realistic for a theme that is for general use.

catch’s picture

Yes the response filter only works for non-big pipe responses, whereas CSS will - so we'd need the CSS for big pipe anyway.

To actually remove the empty regions from the DOM for #36 in a reliable way would require JavaScript. If that's in the header it would be render blocking, so we'd need to make it inline js and compatible with CSPs. Or it could go in the footer, but that would result in Content Layout Shift (at least for regions that the CSS hasn't already hidden).

What I think we should do here is go ahead with the CSS approach because that works reliably for most cases, and open a follow-up to discuss response filters and JavaScript etc. for the remaining edge cases. It would also be possible to implement either or both of those in contrib.