Eleventy: How to address tags within template or rather how to check with Nunjucks if certain tag exists?

Created on 8 May 2019  Β·  23Comments  Β·  Source: 11ty/eleventy

My solution for this (third line) is:

{% if title === "Home" %}
    <div class="home">
{% elseif tags.includes("tag1") %}
    <div class="tag1">
{% else %}
{% endif %}

I am wondering why this does not work:

{% if title === "Home" %}
    <div class="home">
{% endif %}
{% if tags.includes("tag1") %}
    <div class="tag1">
{% endif %}

In general, is the above solution correct?
I am asking this because I get the following error when I extend the above code like this:

{% if title === "Home" %}
    <div class="home">
{% elseif tags.includes("tag1") %}
    <div class="tag1">
{% elseif page.outputPath === "_output/subfolder/index.html" %}
    {% include 'layouts/include.njk' %}
{% else %}
{% endif %}
Error: Unable to call `tags["includes"]`, which is undefined or falsey (Template render error):
education

Most helpful comment

@ssgstarter This is an example of how you could use the filter. See nunjucks filter docs too.

If you'd named the filter something else when registering it with Eleventy, you'd call it differently.

If you did this:

eleventyConfig.addFilter('has_tag', includesFilter)

Then you'd use it like this:

{% set postslist = collections.cases | has_tag("data.tags", "tag1" ) %}

In @danfascia's example usage:

  • First you're creating a variable called postslist and assigning it to Eleventy's filtered collection of items (collections.cases in @danfascia's case, probably collections.posts in many others' cases). The first half of the line, before the | character.

{% set postslist = collections.cases %}

  • But then you're sending that collection through the new includes filter (or has_tag filter if you named it that), which filters the collection to remove any items that don't have the tag ("tag1" in the above example; the tag variable in @danfascia's usage example). The second half of the line, after the | character.

{% set postslist = collections.posts | includes("data.tags", "tag1") %}

  • Then you're simply looping over the resulting filtered collection of items, using a variable called post to represent the current iteration, and including a file that expects a post variable.
{% for post in postslist %}
  {% include "post.njk" %}
{% endfor %}

I think you just need to spend some time getting to know nunjucks filters. Have a look through nunjuck's built-in filters to get the idea. The includes filter is a custom filter.


A potential example for your use case:

{% if title === "Home" %}
    <div class="home">
{% elseif tags | includes(tags, "tag1") %}
    <div class="tag1">
{% endif %}

All 23 comments

Where does the tags variable come from?

As part of the Front Matter.

Could you share that part?
E.g. is it a list [] or a dict {} (in Python parlance).

On some pages I use single tags:

---
tags: tag1
---

on some others multiple tags on multiple lines;

---
tags: 
    - tag1
    - tag2
    - tag3
---

What I search for is a safe 11ty and Nunjucks solution to check if "tags" contains "tag1" (in Liquid parlance).

Can't reproduce.

Using a directory structure like this:

.
β”œβ”€β”€ _includes
β”‚Β Β  └── tag.njk
β”œβ”€β”€ index.md
β”œβ”€β”€ package.json
β”œβ”€β”€ package-lock.json
β”œβ”€β”€ _site
β”‚Β Β  β”œβ”€β”€ index.html
β”‚Β Β  └── test_with_tags
β”‚Β Β      β”œβ”€β”€ foreign
β”‚Β Β      β”‚Β Β  └── index.html
β”‚Β Β      β”œβ”€β”€ multiple
β”‚Β Β      β”‚Β Β  └── index.html
β”‚Β Β      └── single
β”‚Β Β          └── index.html
└── test_with_tags
    β”œβ”€β”€ foreign.md
    β”œβ”€β”€ multiple.md
    └── single.md

with _includes/tag.njk defined as

---
---
{% if title === "Home" %}
    <div class="home">Home</div>
{% endif %}
{% if tags.includes("tag1") %}
    <div class="tag1">
{% else %}
    <div>
{% endif %}
Tags: {{ tags }}
{{ content | safe }}
    </div>

index.md being a plain Hello World, single.md being

---
layout: tag
tags: tag1
---
I'm single

and multiple.md as

---
layout: tag
tags:
  - tag1
  - tag2
---
I'm multiple.

resp. foreign.md as

---
layout: tag
tags: tag2
---
I don't have tag1

yields the following HTML:

    <div class="tag1">

Tags: tag1
<p>I'm single</p>

    </div>
    <div class="tag1">

Tags: tag1,tag2
<p>I'm multiple.</p>

    </div>
    <div>

Tags: tag2
<p>I don't have tag1</p>

    </div>

On a side note: {% elif "tag1" in tags %} might already do the trick.

Thank you very much, @Ryuno-Ki for notes. Back in the house now.

Indeed, the deeper I get the stranger things become ...

To sum up:

{% elseif "tag1" in tags %}                {# works #} 
{% if "tag1" in tags %}                    {# crashes #} 



md5-7728e2572922fdaa562e8bf49a0afa3f



```Nunjucks
{% if tags.includes("tag1") %}             {# crashes #} 



md5-fd25907dc35bffb7f6aedaf6938be0a4



```Nunjucks
{% elseif "tag1" in tags %}                {# works #} 
   do something
{% elseif page.outputPath === "output/path/to/file/with/tag9" %}                {# crashes #} 
   do something else

Hm, I recall that some variables behave differently on the home page vs. all other pages.

That is title is only set on the homepage and available as page.data.title (from frontmatter) on all others.

Maybe check for existence of tags first and wrap the inclusion checks inside?

Very interesting aspects, @Ryuno-Ki.

Given the documentation, I have thought so far that only title does exist, but not page.data.title resp. let us call it item.data.title exists:

                                                    {# works #}

{% if title === "AnyPageTitle" %}                   {# to check any page title #}
   do something
{% elseif "tag1" in tags %}  
   do something
{% else %}
    do something
{% endif %}

```Nunjucks
{# works not for tags, but works for title also #}

{% for item in collections.all %} {# e.g. looping through all collections #}
{% if item.data.title === "AnyPageTitle" %} {# to check any page title #}
do something
{% elseif item.data.tags === "tag1" %}
do something
{% else %}
do something
{% endif %}
{% endfor %}

```Nunjucks
                                                    {# crashes #}

{% for item in collections.all %}                   {# e.g. looping through all collections #}
    {% if item.data.title === "AnyPageTitle" %}     {# to check any page title #}  
        do something
    {% elseif "tag1" in tags %}  
        do something
    {% else %}
        do something
    {% endif %}
{% endfor %}

Even though it is expected, the third example does not work for some reasons. these could be:

  1. As for me, it is a little bit confusing when to use which title variable.
  2. Especially tags are so tricky because "they" could be a string or an array.

I'm pretty sure Eleventy casts tags as arrays, even if written as strings in front matter yaml. (Can't find that in the docs atm, but quite sure I read it somewhere in there.)

Nunjucks isn't straight javascript, so I'd be surprised if {% tags.includes('tag') %} works at all. (edit: Hence the error about tags["includes"])

  • Use item.data.title when item is a Collection item.
  • Use title directly when in the file that defines title in its front matter
  • Use title in an included file when it's included from a template file that defines front matter. (The included file acts as if it were written in the original template file in the first place; it inherits the current variable scope of its caller.)

You could set up a nunjucks or universal filter to check if tag exists on the collection item.

Or just loop through the items tags and set a flag if the tag in question exists.

{% set found = false %}

{% for item in collections.all %}
  {% for tag in item.data.tags %}
    {% if tag === 'tag1' %}{% set found = true %}{% endif %}
  {% endfor %}
  {% if found %} here we are tag1 {% endif %}
{% endfor %}

Very useful hints @jevets. Thank you.

The conclusion I draw from this excursion on the whole is that I am tending to get back to where I was starting from, i e. to separate conditions into several layout templates when it comes to produce something concrete.
The experiment was to find a way to do all this with one layout template (and several conditions).

The puzzling thing is that some situations work unexpectedly and some do not work with or without errors. To give some insights from a beginner`s perspective, some reasons for giving up are:

  • tags seem to be contradictory in syntax and use (string vs. array, conditional vs. loop)
  • title seems to be contradictory in syntax and use (layout vs. include, conditional vs. loop like collection.all)
  • dimmish with layouts when to use and/or mix conditionals and/or loops

And in addition:

  • {% tags.includes('tag') %} works without errors and fails with and without errors in various situations even it is javascript within Nunjucks

I like Eleventy very much. It is such an elegant and powerful tool.

As a beginner, you can achieve success quickly, but you can also fail quickly against the background of the documentation's status quo.

Much more transparency is needed here to let the diamond shine.

I don't think there is a good native way to do this.

I use this filter:
https://github.com/danfascia/radiologymasters/blob/master/11ty/filters/includes.js

@danfascia:
This is what I was talking about in my last posting.
Your file does not exist.

@danfascia Is that a private repo, maybe?

Sorry, it is a private repo, here is the filter code to be placed in includes.js and imported in via the eleventy config as a universal filter

/**
 * Select objects in array whose key includes a value
 *
 * @param {Array} arr Array to test
 * @param {String} key Key to inspect
 * @param {String} value Value key needs to include
 * @return {String} Filtered array
 *
 */
module.exports = function (arr, key, value) {
  return arr.filter(item => {
    const keys = key.split('.');
    const reduce = keys.reduce((object, key) => {
      return object[key];
    }, item);
    const str = String(reduce);

    return (str.includes(value) ? item : false);
  });
};

Here is an example of how to use it, since I always find that the missing piece of the 11ty docs.

{% set postslist = collections.cases | includes("data.modalities", tag ) %}
          {%- if postslist.length > 0 -%}
            {% for case in postslist %}
            {% include "case-list-item.njk" %}
            {% endfor %}
         {%- endif -%}

Looks interesting, @danfascia.

In this constellation, how would the important Universal filter part look like? I guess:

eleventyConfig.addFilter( "INCLUDE_FILTER", function( arr, key, value ) {
    return arr.filter( item => {
        const keys = key.split( '.' );
        const reduce = keys.reduce( ( object, key ) => {
            return object[ key ];
        }, item );
        const str = String( reduce );

        return ( str.includes( value ) ? item : false );
    } );
} );

If so, include.js would become redundant, would it not?

May I ask you to explain the {% set %} line (in regard to the Universal filter and the general question of this thread). I have never seen such a notion before. Is there no need to close it by {% endset %}?

@ssgstarter {% set %} sets a variable into the current context.

Commonly used in two ways. See the nunjucks docs for more.

{% set foo = "some string" %}

{% set bar %}
  some string and the contents of an `include`
  {% include "file.njk" %}
{% endset %}

To import the filter, you'd need to do something like this:

// .eleventy.js

const includesFilter = require("./path/to/your/copy/of/includes.js")

module.exports = function (eleventyConfig) {
  eleventyConfig.addFilter('includes', includesFilter)
  // you could name it whatever you want, i.e.
  // eleventyConfig.addFilter('has_tag', includesFilter)
}

Thank you, @jevets for elucidations. Especially the second part of your hint.

{% set %} in general is clear, I mean rather the whole line:

{% set postslist = collections.cases | includes("data.modalities", tag ) %}

@ssgstarter This is an example of how you could use the filter. See nunjucks filter docs too.

If you'd named the filter something else when registering it with Eleventy, you'd call it differently.

If you did this:

eleventyConfig.addFilter('has_tag', includesFilter)

Then you'd use it like this:

{% set postslist = collections.cases | has_tag("data.tags", "tag1" ) %}

In @danfascia's example usage:

  • First you're creating a variable called postslist and assigning it to Eleventy's filtered collection of items (collections.cases in @danfascia's case, probably collections.posts in many others' cases). The first half of the line, before the | character.

{% set postslist = collections.cases %}

  • But then you're sending that collection through the new includes filter (or has_tag filter if you named it that), which filters the collection to remove any items that don't have the tag ("tag1" in the above example; the tag variable in @danfascia's usage example). The second half of the line, after the | character.

{% set postslist = collections.posts | includes("data.tags", "tag1") %}

  • Then you're simply looping over the resulting filtered collection of items, using a variable called post to represent the current iteration, and including a file that expects a post variable.
{% for post in postslist %}
  {% include "post.njk" %}
{% endfor %}

I think you just need to spend some time getting to know nunjucks filters. Have a look through nunjuck's built-in filters to get the idea. The includes filter is a custom filter.


A potential example for your use case:

{% if title === "Home" %}
    <div class="home">
{% elseif tags | includes(tags, "tag1") %}
    <div class="tag1">
{% endif %}

Awesome, @jevets! πŸ₯‡ Thank you so much for these deep explanations.

Now I can read and understand the different meaning of includes in @danfascia's πŸ₯‡ great example (who I also want to say thank you for sharing these excellent snippets).

@zachleat I can imagine that other people (especially beginners like me) would be glad to find their both revealing aspects in the official documentation. By the way, I am a big fan of your work!


What remains is somehow Faustian: to use or not to use? Maybe a question of further use cases ...

related to @Ryuno-Ki's great hint: somehow pure and legible without any custom filter etc.

{% elseif "tag1" in tags %}

or related @danfascia's little bit extensive code and much harder to read

{% elseif tags | includes(tags, "tag1") %}

Couple of things here:

Most importantly, Eleventy transforms tags to always be an array. You should never encounter a string tags property. This may be a bit confusing in your tests above because JavaScript includes both a String and Array includes method.

Secondly, based on some of the discussion in this issue I added all of these tests and they all pass fine:

test("Nunjucks Test if statements on arrays (Issue #524)", async t => {
  let tr = new TemplateRender("njk", "./test/stubs/");

  t.is(
    await tr.render("{% if 'first' in tags %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if 'sdfsdfs' in tags %}{% else %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if false %}{% elseif 'first' in tags %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if tags.includes('first') %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if tags.includes('dsds') %}{% else %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );

  t.is(
    await tr.render("{% if false %}{% elseif tags.includes('first') %}Success.{% endif %}", {
      tags: ["first", "second"]
    }),
    "Success."
  );
});

So you shouldn’t need a filter for this but if you want to use a filter that’s okay too.

Thanks everyone for your help here!

This is an automated message to let you know that a helpful response was posted to your issue and for the health of the repository issue tracker the issue will be closed. This is to help alleviate issues hanging open waiting for a response from the original poster.

If the response works to solve your problemβ€”great! But if you’re still having problems, do not let the issue’s closing deter you if you have additional questions! Post another comment and I will reopen the issue. Thanks!

Hmm, re-reading the original comment I think it might just be a baseline JS syntax misunderstanding. You’re likely trying to run includes on a template that doesn’t include a tags property (or is null like tags:).

{% if title === "Home" %}
    <div class="home">
{% elseif tags.includes("tag1") %}
    <div class="tag1">
{% else %}
{% endif %}

short circuits when title === "Home" (never running the elseif)

{% if title === "Home" %}
    <div class="home">
{% endif %}
{% if tags.includes("tag1") %}
    <div class="tag1">
{% endif %}

does not short circuit.

Maybe also related to https://github.com/11ty/eleventy/issues/556 which @edwardhorsford fixed for the next version.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

zachleat picture zachleat  Β·  3Comments

aaronstezycki picture aaronstezycki  Β·  3Comments

zachleat picture zachleat  Β·  3Comments

AjitZero picture AjitZero  Β·  3Comments

DirtyF picture DirtyF  Β·  3Comments