Skip to main content

Ensuring Drupal 8 Block Cache Tags bubble up to the Page

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.

by Lee Rowlands /

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:

Response headers

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.

Posted by Lee Rowlands
Senior Drupal Developer

Dated

Comments

Comment by Anonymous

Dated

Love this find. Can we fix Twig templates so caching doesn't break when you don't use this trick? Is there an issue?

Comment by Anonymous

Dated

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.

Comment by Lee Rowlands

Dated

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.

Comment by Jason Want

Dated

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

Comment by Lee Rowlands

Dated

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.

Comment by Jason Want

Dated

Thanks for the response!

Comment by Darvanen

Dated

Fantastic, thank you for publishing this. I wonder if there's a similar trick with views cache.

Comment by Elijah Lynn

Dated

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.

https://www.drupal.org/node/2660002#comment-11840635

Comment by Alexander

Dated

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

Comment by Lee Rowlands

Dated

I recommend the renderviz module, it helps you debug your cache tags

Comment by Alexander

Dated

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?

Comment by Lee Rowlands

Dated

No, something in the $content will have the tags, you just need to ensure it is rendered

Comment by Alexander

Dated

I don't know where I can change the max-age value, which is always equal to -1?

Comment by Avi Schwab

Dated

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?

Comment by Lee Rowlands

Dated

Not sure I understand, but if it works, I guess so

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.