Repository: 11ty/eleventy
Pull request #2482 (2022-07-01 14:37:13)

#1175 and #2287 Allow eleventyExcludeFromCollections to take a list of tags
This is a first implementation for #1175 and #2287 to gather feedback.

This change allows for more fine grained control over how a template is excluded from collections by allowing to set eleventyExcludeFromCollections to a tag or list of tags to exclude the template from.

This is meant as a first implementation for feedback. Also I'm not completely sure if excluding the `all` Collection is done correctly.

I only added minimal tests, but probably some more are needed.


Patch:
diff --git a/src/Plugins/Pagination.js b/src/Plugins/Pagination.js
index 3cba45ac..1f4a0d38 100755
--- a/src/Plugins/Pagination.js
+++ b/src/Plugins/Pagination.js
@@ -6,6 +6,7 @@ const { isPlainObject } = require("@11ty/eleventy-utils");
 const EleventyBaseError = require("../EleventyBaseError");
 const { DeepCopy } = require("../Util/Merge");
 const { ProxyWrap } = require("../Util/ProxyWrap");
+const TemplateData = require("../TemplateData.js");
 
 class PaginationConfigError extends EleventyBaseError {}
 class PaginationError extends EleventyBaseError {}
@@ -45,16 +46,13 @@ class Pagination {
   }
 
   circularReferenceCheck(data) {
-    if (data.eleventyExcludeFromCollections) {
-      return;
-    }
-
     let key = data.pagination.data;
-    let tags = data.tags || [];
-    for (let tag of tags) {
+    let includedTags = TemplateData.getIncludedTagNames(data);
+
+    for (let tag of includedTags) {
       if (`collections.${tag}` === key) {
         throw new PaginationError(
-          `Pagination circular reference${this.inputPathForErrorMessages}, data:\`${key}\` iterates over both the \`${tag}\` tag and also supplies pages to that tag.`
+          `Pagination circular reference${this.inputPathForErrorMessages}, data:\`${key}\` iterates over both the \`${tag}\` collection and also supplies pages to that collection.`
         );
       }
     }
diff --git a/src/TemplateCollection.js b/src/TemplateCollection.js
index 9e6bad61..869c7655 100755
--- a/src/TemplateCollection.js
+++ b/src/TemplateCollection.js
@@ -1,6 +1,7 @@
 const multimatch = require("multimatch");
 const Sortable = require("./Util/Sortable");
 const { TemplatePath } = require("@11ty/eleventy-utils");
+const TemplateData = require("./TemplateData.js");
 
 class TemplateCollection extends Sortable {
   constructor() {
@@ -59,26 +60,27 @@ class TemplateCollection extends Sortable {
 
   getFilteredByTag(tagName) {
     return this.getAllSorted().filter((item) => {
-      if (!tagName) {
+      if (
+        !tagName ||
+        TemplateData.getIncludedTagNames(item.data).includes(tagName)
+      ) {
         return true;
-      } else if (Array.isArray(item.data.tags)) {
-        return item.data.tags.some((tag) => tag === tagName);
       }
       return false;
     });
   }
 
   getFilteredByTags(...tags) {
-    return this.getAllSorted().filter((item) =>
-      tags.every((requiredTag) => {
-        const itemTags = item.data.tags;
+    return this.getAllSorted().filter((item) => {
+      let itemTags = TemplateData.getIncludedTagNames(item.data);
+      return tags.every((requiredTag) => {
         if (Array.isArray(itemTags)) {
           return itemTags.includes(requiredTag);
         } else {
           return itemTags === requiredTag;
         }
-      })
-    );
+      });
+    });
   }
 }
 
diff --git a/src/TemplateData.js b/src/TemplateData.js
index 58c04cb6..6be44716 100755
--- a/src/TemplateData.js
+++ b/src/TemplateData.js
@@ -653,6 +653,51 @@ class TemplateData {
       return this.configApiGlobalData.eleventy.serverless.path;
     }
   }
+
+  static getNormalizedExcludedCollections(data) {
+    let excludes = [];
+    if ("eleventyExcludeFromCollections" in data) {
+      if (data.eleventyExcludeFromCollections !== true) {
+        if (Array.isArray(data.eleventyExcludeFromCollections)) {
+          excludes = data.eleventyExcludeFromCollections;
+        } else if (typeof data.eleventyExcludeFromCollections === "string") {
+          excludes = [data.eleventyExcludeFromCollections];
+        }
+      }
+    }
+    return {
+      excludes,
+      excludeAll: data.eleventyExcludeFromCollections === true,
+    };
+  }
+
+  static getIncludedCollectionNames(data) {
+    if ("tags" in data) {
+      let excludes = TemplateData.getNormalizedExcludedCollections(data);
+      if (excludes.excludeAll) {
+        return [];
+      } else {
+        return ["all", ...data.tags].filter(
+          (tag) => !excludes.excludes.includes(tag)
+        );
+      }
+    } else {
+      return ["all"];
+    }
+  }
+
+  static getIncludedTagNames(data) {
+    if ("tags" in data) {
+      let excludes = TemplateData.getNormalizedExcludedCollections(data);
+      if (excludes.excludeAll) {
+        return [];
+      } else {
+        return data.tags.filter((tag) => !excludes.excludes.includes(tag));
+      }
+    } else {
+      return [];
+    }
+  }
 }
 
 module.exports = TemplateData;
diff --git a/src/TemplateMap.js b/src/TemplateMap.js
index 7b1ec277..45d4905c 100644
--- a/src/TemplateMap.js
+++ b/src/TemplateMap.js
@@ -8,6 +8,7 @@ const debug = require("debug")("Eleventy:TemplateMap");
 const debugDev = require("debug")("Dev:Eleventy:TemplateMap");
 
 const EleventyBaseError = require("./EleventyBaseError");
+const TemplateData = require("./TemplateData.js");
 
 class TemplateMapConfigError extends EleventyBaseError {}
 
@@ -124,22 +125,17 @@ class TemplateMap {
         graph.addNode(entry.inputPath);
       }
 
-      if (!entry.data.eleventyExcludeFromCollections) {
-        // collections.all
-        graph.addDependency(tagPrefix + "all", entry.inputPath);
-
-        if (entry.data.tags) {
-          for (let tag of entry.data.tags) {
-            let tagWithPrefix = tagPrefix + tag;
-            if (!graph.hasNode(tagWithPrefix)) {
-              graph.addNode(tagWithPrefix);
-            }
-
-            // collections.tagName
-            // Dependency from tag to inputPath
-            graph.addDependency(tagWithPrefix, entry.inputPath);
-          }
+      for (let collection of TemplateData.getIncludedCollectionNames(
+        entry.data
+      )) {
+        let collectionWithPrefix = tagPrefix + collection;
+        if (!graph.hasNode(collectionWithPrefix)) {
+          graph.addNode(collectionWithPrefix);
         }
+
+        // collections.tagName
+        // Dependency from tag to inputPath
+        graph.addDependency(collectionWithPrefix, entry.inputPath);
       }
     }
 
@@ -175,21 +171,16 @@ class TemplateMap {
         }
         graph.addDependency(entry.inputPath, tagPrefix + paginationTagTarget);
 
-        if (!entry.data.eleventyExcludeFromCollections) {
-          // collections.all
-          graph.addDependency(tagPrefix + "all", entry.inputPath);
-
-          if (entry.data.tags) {
-            for (let tag of entry.data.tags) {
-              let tagWithPrefix = tagPrefix + tag;
-              if (!graph.hasNode(tagWithPrefix)) {
-                graph.addNode(tagWithPrefix);
-              }
-              // collections.tagName
-              // Dependency from tag to inputPath
-              graph.addDependency(tagWithPrefix, entry.inputPath);
-            }
+        for (let collection of TemplateData.getIncludedCollectionNames(
+          entry.data
+        )) {
+          let collectionWithPrefix = tagPrefix + collection;
+          if (!graph.hasNode(collectionWithPrefix)) {
+            graph.addNode(collectionWithPrefix);
           }
+          // collections.tagName
+          // Dependency from tag to inputPath
+          graph.addDependency(collectionWithPrefix, entry.inputPath);
         }
       }
     }
@@ -312,7 +303,7 @@ class TemplateMap {
               (map.data.pagination &&
                 map.data.pagination.addAllPagesToCollections)
             ) {
-              if (!map.data.eleventyExcludeFromCollections) {
+              if (map.data.eleventyExcludeFromCollections !== true) {
                 // TODO do we need .template in collection entries?
                 this.collection.add(page);
               }


Test patch:
diff --git a/test/TemplateCollectionTest.js b/test/TemplateCollectionTest.js
index 770ca34d..946776a3 100644
--- a/test/TemplateCollectionTest.js
+++ b/test/TemplateCollectionTest.js
@@ -18,7 +18,18 @@ function getNewTemplate(filename, input, output, eleventyConfig) {
 }
 
 function getNewTemplateByNumber(num, eleventyConfig) {
-  let extensions = ["md", "md", "md", "md", "md", "html", "njk"];
+  let extensions = [
+    "md",
+    "md",
+    "md",
+    "md",
+    "md",
+    "html",
+    "njk",
+    "md",
+    "md",
+    "md",
+  ];
 
   return getNewTemplateForTests(
     `./test/stubs/collection/test${num}.${extensions[num - 1]}`,
@@ -296,3 +307,23 @@ test("Sort in place (issue #352)", async (t) => {
   t.deepEqual(posts3[1].template, tmpl1);
   t.deepEqual(posts3[2].template, tmpl4);
 });
+
+test("getFilteredByTag with excludes", async (t) => {
+  let eleventyConfig = new TemplateConfig();
+  let tmpl8 = getNewTemplateByNumber(8, eleventyConfig);
+  let tmpl9 = getNewTemplateByNumber(9, eleventyConfig);
+  let tmpl10 = getNewTemplateByNumber(10, eleventyConfig);
+
+  let c = new Collection();
+  await addTemplate(c, tmpl8);
+  await addTemplate(c, tmpl9);
+  await addTemplate(c, tmpl10);
+
+  let posts = c.getFilteredByTag("post");
+  t.is(posts.length, 0);
+
+  let cats = c.getFilteredByTag("office");
+  t.is(cats.length, 2);
+  t.deepEqual(cats[0].template, tmpl9);
+  t.deepEqual(cats[1].template, tmpl10);
+});
diff --git a/test/TemplateMapTest.js b/test/TemplateMapTest.js
index f445e113..059adf04 100644
--- a/test/TemplateMapTest.js
+++ b/test/TemplateMapTest.js
@@ -1275,9 +1275,9 @@ test("TemplateMap circular references (map.templateContent) using eleventyExclud
   t.falsy(map[0].data.collections);
 
   t.deepEqual(tm.getMappedDependencies(), [
+    "./test/stubs/issue-522/excluded.md",
     "./test/stubs/issue-522/template.md",
     "___TAG___all",
-    "./test/stubs/issue-522/excluded.md",
   ]);
 
   await tm.cache();
diff --git a/test/TemplateWriterTest.js b/test/TemplateWriterTest.js
index 27712c91..3823ff78 100644
--- a/test/TemplateWriterTest.js
+++ b/test/TemplateWriterTest.js
@@ -112,7 +112,7 @@ test("_testGetAllTags", async (t) => {
   let templateMap = await tw._createTemplateMap(paths);
   let tags = templateMap._testGetAllTags();
 
-  t.deepEqual(tags.sort(), ["cat", "dog", "post"].sort());
+  t.deepEqual(tags.sort(), ["cat", "dog", "post", "office"].sort());
 });
 
 test("Collection of files sorted by date", async (t) => {
diff --git a/test/stubs/collection/test10.md b/test/stubs/collection/test10.md
new file mode 100644
index 00000000..2aab7966
--- /dev/null
+++ b/test/stubs/collection/test10.md
@@ -0,0 +1,10 @@
+---
+title: Test Title
+tags:
+  - post
+  - office
+eleventyExcludeFromCollections:
+  - post
+---
+
+# Test 1
diff --git a/test/stubs/collection/test8.md b/test/stubs/collection/test8.md
new file mode 100644
index 00000000..5e5cfdd6
--- /dev/null
+++ b/test/stubs/collection/test8.md
@@ -0,0 +1,9 @@
+---
+title: Test Title
+tags:
+  - post
+  - office
+eleventyExcludeFromCollections: true
+---
+
+# Test 1
diff --git a/test/stubs/collection/test9.md b/test/stubs/collection/test9.md
new file mode 100644
index 00000000..bb14559b
--- /dev/null
+++ b/test/stubs/collection/test9.md
@@ -0,0 +1,9 @@
+---
+title: Test Title
+tags:
+  - post
+  - office
+eleventyExcludeFromCollections: post
+---
+
+# Test 1