Whilst working on a Drupal 8 project, we found that cache tags for a Block Content entity embedded in the footer weren't bubbling up to the page cache.
Read on to find out how we debugged this and how you can ensure this doesn't happen to you too.
The problem
On our site we had a Block Content entity embedded in the footer that contained an Entity Reference field which allowed site admins to highlight author profiles. The issue was that if the admin edited this block and changed the referenced authors, or the order of the authors - the changes didn't reflect for anonymous users until the page cache was cleared.
This immediately sounded like an issue with cache tags bubbling.
About cache tags
So what are cache tags. Well lets quote the excellent handbook page:
Cache tags provide a declarative way to track which cache items depend on some data managed by Drupal.
So in our case, as the page is built, all of the content that is rendered has its cache tags bubble up to the page level cache entry. When any of the items that form the page are updated, all cache entries that match that item's tags are flushed. This ensures that if a node or block that forms part of a page is updated, the cache entry for the page is invalidated. For entities, the tags are in the format {entity type}:{entity id} - e.g. node:2 or block_content:7
Clearly this wasn't happening for our block.
Debugging cache tags
So the first step with debugging this issue is to see what cache tags were associated with the page.
Luckily, core lets you do this pretty easily.
In sites/default/services.yml you'll find this line:
http.response.debug_cacheability_headers: false
Simply change it to true, and rebuild your container (clear caches or drush cr). Then browse to your site and view the headers in the Network panel of your developer toolbar. You'll start seeing headers showing you the cache tags like so:
Checkout the X-Drupal-Cache-Tags one to see what tags make up the page.
So in our case we could see that the block we were rendering wasn't showing up in the cache tags.
Digging into the EntityViewBuilder for the block and block content entity, we could see that the right tags were being added to the $content variable, but they just weren't bubbling up to the page level.
Rendering individual fields in Twig templates
Now, this particular block had it's own Twig template, we were controlling the markup to ensure that one field was rendered in a grid layout and another was rendered at the end. The block type had two fields, and we were rendering them using something like this:
<div{{ attributes.addClass('flex-grid__2-col') }}> {% if label %} <h2{{ title_attributes.addClass('section-title--light') }}>{{ label }}</h2> {% endif %} {% block content %} <div class="flex-grid"> {{ content.field_featured_author }} </div> <div class="spacing--small-before"> {{ content.field_more_link }} </div> {% endblock %} </div>
i.e We were rendering just field_featured_author and field_more_link from the content variable. And this is the gotcha. You have to render the content variable to ensure that its cache tags bubble up and end up in the page cache.
The fix
There were only two fields on this block content entity, and we wanted control over how they were output. But we also had to render the content variable to make sure the cache tags bubbled. This was a chance for the Twig without filter to rescue the day. The new markup was:
<div{{ attributes.addClass('flex-grid__2-col') }}> {% if label %} <h2{{ title_attributes.addClass('section-title--light') }}>{{ label }}</h2> {% endif %} {% block content %} {{ content|without('field_featured_author', 'field_more_link') }} <div class="flex-grid"> {{ content.field_featured_author }} </div> <div class="spacing--small-before"> {{ content.field_more_link }} </div> {% endblock %} </div>
In other words, we still render the fields on their own, but we make sure we also render the top-level content variable, excluding the individual fields using the without filter.
After this change, we started seeing our block content cache tags in the page-level cache tags and as to be expected, changing the block triggered the appropriate flushes of the page cache.
Comments
Great article, great gotcha! I've added a link to this article in the official documentation at https://www.drupal.org/developing/api/8/render/arrays/cacheability :)
Nit: s/has it's cache tags/has its cache tags/
Thanks, fixed that
Love this find. Can we fix Twig templates so caching doesn't break when you don't use this trick? Is there an issue?
Could content.field_featured_author contain the cache tag instead of content? That way you wouldn't have to think about this stuff in the twig template.
No, not really - because the field shouldn't be concerned about its parent. Yes, it does know that it is part of a block entity, but it doesn't necessarily change if that block entity is changed.
e.g. if you have three fields on the entity and only one of them changes, it is not efficient to invalidate the render cached content of the other two fields.
The approach when there will be un-rendered fields is nicely documented in a core issue: https://www.drupal.org/node/2660002
Far out, thanks for writing this up! I was curious to know if this applies when rendering node entities.
In a fresh D8 install using the Bartik theme, I commented out the rendering of the content variable in node.html.twig. I was not expecting to see the node:1 cache tag on the path /node, but it was included. I'm guessing the frontpage view, which is rendering the node teaser here, is including its own cache tags, including node:1, that bubbles up to the page. Maybe the cache tags are handled differently here.
Perhaps if I were to programmatically render parts of a node on its own (in a custom block using entity.query service to query, load and then render parts of nodes) , I'd expect the same behavior described here. In which case, I either need to stuff the blocks build render array with appropriate cache tags or use the method above somewhere along the way when rendering the node. Have you come across this? Thanks, Jason
I haven't come across it with nodes yet. Berdir pointed out on twitter that perhaps block content was special, because it was rendered in a block wrapper. So yeah, it might happen any time you render something in a block wrapper.
Thanks for the response!
Fantastic, thank you for publishing this. I wonder if there's a similar trick with views cache.
This is huge Lee! I just spent a few hours debugging this and arrived at the same conclusion, that the block_content:xx was not making it to the cacheability metadata for the page but was wondering why!
I would like a cleaner solution than this though. There are going to be many people who have this issue and will have no idea what is going on and that it was caused by Twig.
thanks for your article!
I tried your solution on my project, but It does not work.
I explain my case:
I use paragraphs module to create & edit content. Paragraphs can be basic text, multiple fields ou file entities. To my needs, I had to overload twig templates (for example file--document.html.twig). So, when a user add a file in his node, the file is rendered with the twig template.
When this user edit the file (title or description for example), the user has also to edit the node to see changes.
Is there a solution to avoid this action?
I hope I'm clear, in spite my poor english :)
Thanks!
Alexander
I recommend the renderviz module, it helps you debug your cache tags
Thanks for your answer! I did not know this wonderful module!
I saw in the code:
<!--{"tags":["file_view"],"contexts":["languages:language_interface","theme","user.permissions"],"max-age":-1,"keys":["entity_view","file","7","default"],"bin":"render"}-->
Do I have to create another preprocess function to force cache tags?
1) Thank you for this. I've been banging my head against caching issues for a while now and this solved it.
2) I'm working with a template that has a bunch of fields, and it seems like enumerating all of the fields to the without filter would be a PITA. I was able to trigger the same behavior with {% set catch_cache = content|render %} ... do you see any problem with doing that?
Not sure I understand, but if it works, I guess so
Comment by Wim Leers
Dated