Repository: ProjectEvergreen/greenwood (js)

Issue:
support an array for collections frontmatter key
<!--
# PLEASE READ FIRST
If asking a question, looking for help, or you have an idea for Greenwood, please consider opening a discussion first or chatting with us in our Discord.
-->

## Type of Change

Enhancement

## Summary

Currently, Greenwood supports defining [content collections](https://greenwoodjs.dev/docs/content-as-data/collections/) through a frontmatter key
```md
---
collection: navigation
order: 2
---

# About Page

Lorum Ipsum
```

However, it was observed / requested that content could belong in more than one collection, so allowing it be an array would be really nice.

```md
---
collection:
  - navigation
  - other
order: 2
---

# About Page

Lorum Ipsum
```

## Details

Aside from implementing this, would also want to update the documentation to demonstrate this capability / option.

Pull request description:
enhancement/issue 1333 add support for `collection` frontmatter key as an array
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue

resolves #1333 

## Documentation 

1. [x] https://github.com/ProjectEvergreen/www.greenwoodjs.dev/issues/189

## Summary of Changes

1. Add support for frontmatter `collection` property to be an array (or string)
1. Update `Frontmatter` typings

## TODO
1. [x] Rebase and update type definitions after #1420 is merged

Patch:
diff --git a/packages/cli/src/lib/content-utils.js b/packages/cli/src/lib/content-utils.js
index 090a809c..93e5c0c7 100644
--- a/packages/cli/src/lib/content-utils.js
+++ b/packages/cli/src/lib/content-utils.js
@@ -32,7 +32,15 @@ function cleanContentCollection(collection = []) {
 }
 
 function filterContentByCollection(graph, collection) {
-  return graph.filter((page) => page?.data?.collection === collection);
+  return graph.filter((page) => {
+    const pageCollection = page?.data?.collection;
+
+    if (typeof pageCollection === "string") {
+      return pageCollection === collection;
+    } else if (Array.isArray(pageCollection)) {
+      return pageCollection.includes(collection);
+    }
+  });
 }
 
 function filterContentByRoute(graph, route) {
diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js
index dcecae0a..82018af5 100644
--- a/packages/cli/src/lifecycles/graph.js
+++ b/packages/cli/src/lifecycles/graph.js
@@ -216,7 +216,7 @@ const generateGraph = async (compilation) => {
               /*
                * Custom front matter - Variable Definitions
                * --------------------------------------------------
-               * collection: the name of the collection for the page
+               * collection: the name of the collection for the page (as a string or array)
                * order: the order of this item within the collection
                * tocHeading: heading size to use a Table of Contents for a page
                * tableOfContents: json object containing page's table of contents (list of headings)
@@ -289,11 +289,21 @@ const generateGraph = async (compilation) => {
               const pageCollection = customData.collection;
 
               if (pageCollection) {
-                if (!collections[pageCollection]) {
-                  collections[pageCollection] = [];
-                }
+                if (typeof pageCollection === "string") {
+                  if (!collections[pageCollection]) {
+                    collections[pageCollection] = [];
+                  }
+
+                  collections[pageCollection].push(page);
+                } else if (Array.isArray(pageCollection)) {
+                  pageCollection.forEach((collection) => {
+                    if (!collections[collection]) {
+                      collections[collection] = [];
+                    }
 
-                collections[pageCollection].push(page);
+                    collections[collection].push(page);
+                  });
+                }
               }
 
               compilation.collections = collections;
diff --git a/packages/cli/src/types/compilation.d.ts b/packages/cli/src/types/compilation.d.ts
index 346d1882..cf83c8b2 100644
--- a/packages/cli/src/types/compilation.d.ts
+++ b/packages/cli/src/types/compilation.d.ts
@@ -19,12 +19,12 @@ export type Compilation = {
 };
 
 export type Frontmatter = {
-  collection?: string;
+  collection?: string | string[];
   label?: string;
   layout?: string;
   title?: string;
   imports?: string[];
-  data: {
+  data?: {
     [key: string]: string;
   };
 };


Test patch:
diff --git a/packages/cli/test/cases/build.config.prerender-collections/build.config.prerender-collections.spec.js b/packages/cli/test/cases/build.config.prerender-collections/build.config.prerender-collections.spec.js
index f89761c7..07972024 100644
--- a/packages/cli/test/cases/build.config.prerender-collections/build.config.prerender-collections.spec.js
+++ b/packages/cli/test/cases/build.config.prerender-collections/build.config.prerender-collections.spec.js
@@ -18,6 +18,7 @@
  *  src/
  *   components/
  *    blog-posts-lists.js
+ *    footer.js
  *    header.js
  *    toc.js
  *   pages/
@@ -130,7 +131,7 @@ describe("Build Greenwood With: ", function () {
 
         it("should have the expected inline active frontmatter collection data", function () {
           const collection = JSON.parse(
-            dom.window.document.querySelector("body span").textContent,
+            dom.window.document.querySelector("body span#nav").textContent,
           ).sort((a, b) => (a.data.order > b.data.order ? 1 : -1));
 
           expect(collection[0].route).to.equal("/");
@@ -149,6 +150,44 @@ describe("Build Greenwood With: ", function () {
           expect(collection[2].id).to.equal("toc");
         });
       });
+
+      describe("footer links from getContentByCollection array items", function () {
+        let linkItems;
+
+        before(function () {
+          linkItems = dom.window.document.querySelectorAll("footer ul li a");
+        });
+
+        it("should have the expected number of nav links from all pages in the collection", function () {
+          expect(linkItems.length).to.equal(2);
+        });
+
+        it("should have the expected link content from all pages in the collection", function () {
+          expect(linkItems[0].getAttribute("href")).to.equal("/blog/");
+          expect(linkItems[0].getAttribute("title")).to.equal("Blog");
+          expect(linkItems[0].textContent).to.equal("Blog");
+
+          expect(linkItems[1].getAttribute("href")).to.equal("/toc/");
+          expect(linkItems[1].getAttribute("title")).to.equal("Table of Contents");
+          expect(linkItems[1].textContent).to.equal("Table of Contents");
+        });
+
+        it("should have the expected inline active frontmatter collection data", function () {
+          const collection = JSON.parse(
+            dom.window.document.querySelector("body span#footer").textContent,
+          );
+
+          expect(collection[0].route).to.equal("/blog/");
+          expect(collection[0].title).to.equal("Blog");
+          expect(collection[0].label).to.equal(collection[0].title);
+          expect(collection[0].id).to.equal("blog-index");
+
+          expect(collection[1].route).to.equal("/toc/");
+          expect(collection[1].title).to.equal("Table of Contents");
+          expect(collection[1].label).to.equal(collection[1].title);
+          expect(collection[1].id).to.equal("toc");
+        });
+      });
     });
 
     describe("Default output for blog/index.html with routes based collection content", function () {
diff --git a/packages/cli/test/cases/build.config.prerender-collections/src/components/footer.js b/packages/cli/test/cases/build.config.prerender-collections/src/components/footer.js
new file mode 100644
index 00000000..f12ef474
--- /dev/null
+++ b/packages/cli/test/cases/build.config.prerender-collections/src/components/footer.js
@@ -0,0 +1,25 @@
+import { getContentByCollection } from "@greenwood/cli/src/data/client.js";
+
+export default class Footer extends HTMLElement {
+  async connectedCallback() {
+    const linkItems = await getContentByCollection("footer");
+
+    this.innerHTML = `
+      <footer>
+        <ul>
+          ${linkItems
+            .map((item) => {
+              const { route, label, title } = item;
+
+              return `
+                <li><a href="${route}" title="${title}">${label}</a></li>
+              `;
+            })
+            .join("")}
+        </ul>
+      </footer>
+    `;
+  }
+}
+
+customElements.define("x-footer", Footer);
diff --git a/packages/cli/test/cases/build.config.prerender-collections/src/pages/blog/index.html b/packages/cli/test/cases/build.config.prerender-collections/src/pages/blog/index.html
index ed941850..f7799862 100644
--- a/packages/cli/test/cases/build.config.prerender-collections/src/pages/blog/index.html
+++ b/packages/cli/test/cases/build.config.prerender-collections/src/pages/blog/index.html
@@ -1,5 +1,7 @@
 ---
-collection: nav
+collection:
+  - nav
+  - footer
 order: 2
 ---
 
diff --git a/packages/cli/test/cases/build.config.prerender-collections/src/pages/index.html b/packages/cli/test/cases/build.config.prerender-collections/src/pages/index.html
index 4083fb2e..78c23dba 100644
--- a/packages/cli/test/cases/build.config.prerender-collections/src/pages/index.html
+++ b/packages/cli/test/cases/build.config.prerender-collections/src/pages/index.html
@@ -6,11 +6,14 @@ order: 1
 <html>
   <head>
     <script type="module" src="../components/header.js" data-gwd-opt="static"></script>
+    <script type="module" src="../components/footer.js" data-gwd-opt="static"></script>
   </head>
 
   <body>
     <x-header></x-header>
-    <span>${globalThis.collection.nav}</span>
+    <span id="nav">${globalThis.collection.nav}</span>
+    <span id="footer">${globalThis.collection.footer}</span>
     <h1>Home Page</h1>
+    <x-footer></x-footer>
   </body>
 </html>
diff --git a/packages/cli/test/cases/build.config.prerender-collections/src/pages/toc.html b/packages/cli/test/cases/build.config.prerender-collections/src/pages/toc.html
index 787fb1b3..25a879b4 100644
--- a/packages/cli/test/cases/build.config.prerender-collections/src/pages/toc.html
+++ b/packages/cli/test/cases/build.config.prerender-collections/src/pages/toc.html
@@ -1,5 +1,7 @@
 ---
-collection: nav
+collection:
+  - nav
+  - footer
 order: 3
 label: Table of Contents
 ---