Repository: ProjectEvergreen/greenwood (js)

Issue:
support passing front matter from pages to graph (custom content metadata in markdown) 
## Type of Change
- [ ] New Feature Request
- [ ] Documentation / Website
- [x] Improvement / Suggestion
- [ ] Bug
- [ ] Other (please clarify below)

## Summary
In certain cases, having arbitrary data set in a markdown file available through the graph and out the other side would be awesome.

## Details
Something like [this](https://github.com/ProjectEvergreen/greenwood/issues/115#issuecomment-522336642)
```md
---
template: 'blog'
date: '2019-09-01'
title: 'Blog Post Title'
---
```

Pull request:
Enhancement/issue 274 custom frontmatter as data
<!--
## Submitting a Pull Request
We love contributions and appreciate any help you can offer!
-->

## Related Issue
resolves #274 (and in support of work being done in #295, since I think this makes for a better API for passing in "menu" related data )

## Summary of Changes
1. CLI graph now supports a generic `data` property for all custom front matter in a _.md_ file
1. GraphQL server now supports dynamic frontmatter data from Greenwood `graph`
1. GraphQL resolvers now support arbitrary `data` from CLI
1. Added new unit tests and test cases for graph + frontmatter and updated mock graph
1. Added **_Custom Data_** section to Front Matter docs and updated to reflect clear "reserved" variables

## TODO
1. [x] Add unit tests
1. [x] Test with additional custom frontmatter in spec, like `author: xxx`
1. [ ] BYOQ (bring your own query)?  Need to see how to [override / merge]() Greenwood's _.gql_ files in _blog-template.js_ in the spec.  Otherwise, users will need to just copy / paste, will likely make an issue for this.
1. [x] Documentation
1. [x] Better way to "prune" custom front matter from `attributes` in graph lifecycle?


Patch:
diff --git a/packages/cli/src/data/schema/config.js b/packages/cli/src/data/schema/config.js
index 595aa0af..a4adea9f 100644
--- a/packages/cli/src/data/schema/config.js
+++ b/packages/cli/src/data/schema/config.js
@@ -1,4 +1,4 @@
-const { gql } = require('apollo-server');
+const gql = require('graphql-tag');
 
 const getConfiguration = async (root, query, context) => {
   return context.config;
diff --git a/packages/cli/src/data/schema/graph.js b/packages/cli/src/data/schema/graph.js
index f1c9f179..314191ed 100644
--- a/packages/cli/src/data/schema/graph.js
+++ b/packages/cli/src/data/schema/graph.js
@@ -1,4 +1,4 @@
-const { gql } = require('apollo-server-express');
+const gql = require('graphql-tag');
 
 const getDeriveMetaFromRoute = (route) => {
   // TODO hardcoded root / depth - #273
@@ -22,7 +22,7 @@ const getPagesFromGraph = async (root, query, context) => {
 
   graph
     .forEach((page) => {
-      const { route, mdFile, fileName, template, title } = page;
+      const { route, mdFile, fileName, template, title, data } = page;
       const { label } = getDeriveMetaFromRoute(route);
       const id = page.label;
 
@@ -32,7 +32,10 @@ const getPagesFromGraph = async (root, query, context) => {
         fileName,
         template,
         title: title !== '' ? title : label,
-        link: route
+        link: route,
+        data: {
+          ...data
+        }
       });
     });
 
@@ -69,7 +72,7 @@ const getChildrenFromParentRoute = async (root, query, context) => {
 
   graph
     .forEach((page) => {
-      const { route, mdFile, fileName, template, title } = page;
+      const { route, mdFile, fileName, template, title, data } = page;
       const { label } = getDeriveMetaFromRoute(route);
       const root = route.split('/')[1];
 
@@ -82,7 +85,10 @@ const getChildrenFromParentRoute = async (root, query, context) => {
           fileName,
           template,
           title: title !== '' ? title : label,
-          link: route
+          link: route,
+          data: {
+            ...data
+          }
         });
       }
     });
@@ -92,6 +98,7 @@ const getChildrenFromParentRoute = async (root, query, context) => {
 
 const graphTypeDefs = gql`
   type Page {
+    data: Data,
     id: String,
     filePath: String,
     fileName: String,
diff --git a/packages/cli/src/data/schema/schema.js b/packages/cli/src/data/schema/schema.js
index 10f7c79a..741756f5 100644
--- a/packages/cli/src/data/schema/schema.js
+++ b/packages/cli/src/data/schema/schema.js
@@ -1,22 +1,50 @@
 const { makeExecutableSchema } = require('apollo-server-express');
 const { configTypeDefs, configResolvers } = require('./config');
 const { graphTypeDefs, graphResolvers } = require('./graph');
+const gql = require('graphql-tag');
 
-const mergedResolvers = Object.assign({}, {
-  Query: {
-    ...configResolvers.Query,
-    ...graphResolvers.Query
-  }
-});
-
-const schema = makeExecutableSchema({
-  typeDefs: [
-    configTypeDefs,
-    graphTypeDefs
-  ],
-  resolvers: [
-    mergedResolvers
-  ]
-});
-
-module.exports = schema;
\ No newline at end of file
+module.exports = (graph) => {
+  let uniqueCustomDataDefKeys = {};
+  let customDataDefs = '';
+
+  graph.forEach((page) => {
+    Object.keys(page.data).forEach(key => {
+      uniqueCustomDataDefKeys[key] = 'String';
+    });
+  });
+
+  Object.keys(uniqueCustomDataDefKeys).forEach((key) => {
+    customDataDefs += `
+      ${key}: ${uniqueCustomDataDefKeys[key]}
+    `;
+  });
+
+  // use noop as a default key since type Data cannot be empty
+  const mergedGraphTypeDefs = gql`
+    type Data {
+      noop: String
+      ${customDataDefs}
+    }
+
+    ${graphTypeDefs}
+  `;
+
+  const mergedResolvers = Object.assign({}, {
+    Query: {
+      ...configResolvers.Query,
+      ...graphResolvers.Query
+    }
+  });
+
+  const schema = makeExecutableSchema({
+    typeDefs: [
+      configTypeDefs,
+      mergedGraphTypeDefs
+    ],
+    resolvers: [
+      mergedResolvers
+    ]
+  });
+
+  return schema;
+};
\ No newline at end of file
diff --git a/packages/cli/src/data/server.js b/packages/cli/src/data/server.js
index 66209250..3dac300b 100644
--- a/packages/cli/src/data/server.js
+++ b/packages/cli/src/data/server.js
@@ -1,11 +1,10 @@
 const { ApolloServer } = require('apollo-server');
-const schema = require('./schema/schema');
-const createCache = require('./cache');
 
 module.exports = (compilation) => {
   const { config, graph, context } = compilation;
+  const schema = require('./schema/schema')(graph);
+  const createCache = require('./cache');
 
-  // Create schema
   const server = new ApolloServer({
     schema,
     playground: {
diff --git a/packages/cli/src/lifecycles/graph.js b/packages/cli/src/lifecycles/graph.js
index f9045ec0..a9598e11 100644
--- a/packages/cli/src/lifecycles/graph.js
+++ b/packages/cli/src/lifecycles/graph.js
@@ -68,7 +68,7 @@ const createGraphFromPages = async (pagesDir, config) => {
                   relativeExpectedPath = `'../${fileName}/${fileName}.js'`;
                 }
 
-                // generate a random element name
+                // generate a random element tag name
                 label = label || generateLabelHash(filePath);
 
                 // set <title></title> element text, override with markdown title
@@ -77,6 +77,7 @@ const createGraphFromPages = async (pagesDir, config) => {
                 /*
                 * Variable Definitions
                 *----------------------
+                * data: custom frontmatter set per page within frontmatter
                 * mdFile: path for an md file which will be imported in a generated component
                 * label: the unique label given to generated component element e.g. <wc-md-somelabel></wc-md-somelabel>
                 * route: route for a given page's url
@@ -88,8 +89,27 @@ const createGraphFromPages = async (pagesDir, config) => {
                 * title: the head <title></title> text
                 * meta: og graph meta array of objects { property/name, content }
                 */
-
-                pages[pagesIndexMap.get(filenameHash)] = { mdFile, label, route, template, filePath, fileName, relativeExpectedPath, title, meta };
+                const customData = attributes;
+
+                // prune "reserved" attributes that are supported by Greenwood
+                // https://www.greenwoodjs.io/docs/front-matter
+                delete customData.label;
+                delete customData.imports;
+                delete customData.title;
+                delete customData.template;
+
+                pages[pagesIndexMap.get(filenameHash)] = {
+                  data: customData || {},
+                  mdFile,
+                  label,
+                  route,
+                  template,
+                  filePath,
+                  fileName,
+                  relativeExpectedPath,
+                  title,
+                  meta
+                };
               }
 
               if (stats.isDirectory()) {
diff --git a/www/components/shelf/docs.json b/www/components/shelf/docs.json
index 89947ceb..93223273 100644
--- a/www/components/shelf/docs.json
+++ b/www/components/shelf/docs.json
@@ -53,16 +53,24 @@
     "path": "/docs/front-matter",
     "items": [
       {
-        "name": "Page Configuration",
-        "id": "page-configuration"
+        "name": "Element Label",
+        "id": "element-label"
+      },
+      {
+        "name": "Imports",
+        "id": "imports"
       },
       {
         "name": "Template",
-        "id": "define-template"
+        "id": "template"
       },
       {
-        "name": "Imports",
-        "id": "imports"
+        "name": "Title",
+        "id": "title"
+      },
+      {
+        "name": "Custom Data",
+        "id": "custom-data"
       }
     ]
   },
diff --git a/www/pages/docs/front-matter.md b/www/pages/docs/front-matter.md
index 655bd284..223147e1 100644
--- a/www/pages/docs/front-matter.md
+++ b/www/pages/docs/front-matter.md
@@ -2,29 +2,36 @@
 
 "Front matter" is a [YAML](https://yaml.org/) block at the top of any markdown file.  It gives you the ability to define variables that are made available to Greenwood's build process. You can also use it to `import` additional files.
 
-#### Example:
+### Element Label
+
+When this markdown file is compiled to a component it will automatically generate a custom element tag name. If you want to use a custom element name of your own for your page, you can give the component a label using the predefined variable `label`.
+
+#### Example
 ```render md
 ---
-title: 'Hello World'
+label: 'mypage'
 ---
 
-# Hello World
-The is a markdown file with title defined in front-matter.
 ```
 
-### Page Configuration
+Which will compile to the element: `<wc-md-mypage></wc-md-mypage>`
+
 
-When this markdown file is compiled to a component it will automatically generate a component definition. If you want to use a custom element name for your component, you can give the component a label using the predefined variable `label`.
+### Imports
+
+If you want to import custom files such as a custom element, you can use the predefined variable `imports`.
 
 #### Example
 ```render md
 ---
-label: 'mypage'
+imports:
+  MyFile: '../components/MyFile/myfile.js'
 ---
 
 ```
 
-Which will compile to the element: `<wc-md-mypage></wc-md-mypage>`
+See our [Markdown Docs](/docs/markdown#imports) for more information about rendering custom elements in markdown files.
+
 
 ### Template
 When creating multiple page templates, you can use the `template` front-matter to configure Greenwood to use that template for a given page.
@@ -44,17 +51,52 @@ In this example, the _src/templates/home-template.js_ will be used to render the
 > **Note:** By default, Greenwood will use `src/templates/page-template.js` for all undefined template pages.
 
 
-### Imports
+### Title
+To set the `<title>` for a given page, you can set the `title` variable.  Otherwise, the `<title>` will be inferred from the file name.
+
+#### Example
+```render md
+---
+title: 'My Blog Post'
+---
+
+# This is a post
+The is a markdown file with title defined in front-matter.
+```
+
+In this example, the `<title>` tag will be the `title`.
+```render html
+<title>My Blog Post</title>
+```
+
+> Note: If you set `title` from your [configuration file](/docs/configuration#title), the output would be 
+> ```render html
+> <title>{ConfigTitle} - My Blog Post</title>
+> ```
+
+### Custom Data 
+
+You can also pass custom data from your markdown file and extract that through the GraphQL server.
 
-If you want to import custom files such as a custom element, you can use the predefined variable `imports`.
 
 #### Example
 ```render md
 ---
-imports:
-  MyFile: '../components/MyFile/myfile.js'
+author: 'Jon Doe'
+date: '04/07/2020'
 ---
+```
 
+You would then need to create a `graph` GraphQL query and use that with Greenwood's built in client to get access to that `data`, plus whatever other fields you might want.
+```render gql
+query {
+  graph {
+    data: {
+      author,
+      date
+    }
+  }
+}
 ```
 
-See our [Markdown Docs](/docs/markdown#imports) for more information about rendering custom elements in markdown files.
\ No newline at end of file
+> See [our docs](https://deploy-preview-284--elastic-blackwell-3aef44.netlify.com/docs/data#internal-sources) on using GraphQL w/Greenwood for more information on querying for data.
\ No newline at end of file


Test patch:
diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js b/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js
new file mode 100644
index 00000000..9114f311
--- /dev/null
+++ b/packages/cli/test/cases/build.data.graph-custom-frontmatter/build.data.graph-custom-frontmatter.spec.js
@@ -0,0 +1,109 @@
+/*
+ * Use Case
+ * Run Greenwood build command with GraphQL calls to get data about the projects graph.
+ *
+ * User Result
+ * Should generate a Greenwood build that specifically tests for custom frontmatter set by individual pages.
+ * 
+ * User Command
+ * greenwood build
+ *
+ * Default Config
+ *
+ * Custom Workspace
+ * src/
+ *   pages/
+ *     blog/
+ *       first-post.md
+ *       second-post.md
+ *     index.md
+ *   templates/
+ *     blog-template.js
+ */
+const expect = require('chai').expect;
+const fs = require('fs');
+const glob = require('glob-promise');
+const { JSDOM } = require('jsdom');
+const path = require('path');
+const TestBed = require('../../../../../test/test-bed');
+
+describe('Build Greenwood With: ', function() {
+  const LABEL = 'Data from GraphQL and using Custom Frontmatter Data';
+  const apolloStateRegex = /window.__APOLLO_STATE__=({.*?});/;
+  let setup;
+
+  before(async function() {
+    setup = new TestBed();
+    this.context = await setup.setupTestBed(__dirname);
+  });
+
+  describe(LABEL, function() {
+
+    before(async function() {
+      await setup.runGreenwoodCommand('build');
+    });
+
+    runSmokeTest(['public', 'not-found'], LABEL);
+
+    describe('Blog Page (Template) w/ custom date', function() {
+      beforeEach(async function() {
+        dom = await JSDOM.fromFile(path.resolve(this.context.publicDir, 'blog', 'first-post', 'index.html'));
+      });
+
+      it('should output an index.html file (first post page)', function() {
+        expect(fs.existsSync(path.join(this.context.publicDir, 'blog', 'first-post', 'index.html'))).to.be.true;
+      });
+
+      it('should output one cache.json file', async function() {
+        expect(await glob.promise(path.join(this.context.publicDir, 'blog', 'cache.json'))).to.have.lengthOf(1);
+      });
+
+      it('should output one cache.json file to be defined', function() {
+        const cacheContents = require(path.join(this.context.publicDir, 'blog', 'cache.json'));
+
+        expect(cacheContents).to.not.be.undefined;
+      });
+
+      it('should have one window.__APOLLO_STATE__ <script> with (approximated) expected state', () => {
+        const scriptTags = dom.window.document.querySelectorAll('head > script');
+        const apolloScriptTags = Array.prototype.slice.call(scriptTags).filter(script => {
+          return script.getAttribute('data-state') === 'apollo';
+        });
+
+        expect(apolloScriptTags.length).to.be.equal(1);
+        expect(apolloScriptTags[0].innerHTML).to.match(apolloStateRegex);
+      });
+
+      it('should have expected blog posts links in the <body> tag when using ChildrenQuery', function() {
+        const listItems = dom.window.document.querySelectorAll('body div.posts ul li');
+        const linkItems = dom.window.document.querySelectorAll('body div.posts ul li a');
+        const spanItems = dom.window.document.querySelectorAll('body div.posts ul li span');
+
+        expect(listItems.length).to.be.equal(2);
+        expect(linkItems.length).to.be.equal(2);
+        expect(spanItems.length).to.be.equal(2);
+
+        const link1 = linkItems[0];
+        const link2 = linkItems[1];
+        const span1 = spanItems[0];
+        const span2 = spanItems[1];
+
+        expect(link1.href.replace('file://', '')).to.be.equal('/blog/first-post/');
+        expect(link1.title).to.be.equal('Click to read my Blog blog post');
+        expect(link1.innerHTML).to.contain('Blog posted: 2020/04/05');
+        expect(span1.innerHTML).to.contain('Author: Lorum');
+
+        expect(link2.href.replace('file://', '')).to.be.equal('/blog/second-post/');
+        expect(link2.title).to.be.equal('Click to read my Blog blog post');
+        expect(link2.innerHTML).to.contain('Blog posted: 2020/04/06');
+        expect(span2.innerHTML).to.contain('Author: Ipsum');
+      });
+    });
+
+  });
+
+  after(function() {
+    setup.teardownTestBed();
+  });
+
+});
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/first-post.md b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/first-post.md
new file mode 100644
index 00000000..600ba45d
--- /dev/null
+++ b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/first-post.md
@@ -0,0 +1,10 @@
+---
+template: 'blog'
+date: '2020/04/05'
+author: 'Lorum'
+---
+
+## My First Blog Post
+Lorem Ipsum
+
+[back](/)
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/second-post.md b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/second-post.md
new file mode 100644
index 00000000..833fe624
--- /dev/null
+++ b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/blog/second-post.md
@@ -0,0 +1,10 @@
+---
+template: 'blog'
+date: '2020/04/06'
+author: 'Ipsum'
+---
+
+## My Second Blog Post
+Lorem Ipsum
+
+[back](/)
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/index.md b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/index.md
new file mode 100644
index 00000000..b38fc3cf
--- /dev/null
+++ b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/pages/index.md
@@ -0,0 +1,7 @@
+## Home Page
+
+This is the Getting Started home page!
+
+### My Posts
+- [my-second-post](/blog/second-post/)
+- [my-first-post](/blog/first-post/)
diff --git a/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/templates/blog-template.js b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/templates/blog-template.js
new file mode 100644
index 00000000..28ce6074
--- /dev/null
+++ b/packages/cli/test/cases/build.data.graph-custom-frontmatter/src/templates/blog-template.js
@@ -0,0 +1,80 @@
+import { html, LitElement } from 'lit-element';
+import client from '@greenwood/cli/data/client';
+import gql from 'graphql-tag';
+
+MDIMPORT;
+
+class BlogTemplate extends LitElement {
+
+  static get properties() {
+    return {
+      posts: {
+        type: Array
+      }
+    };
+  }
+
+  constructor() {
+    super();
+    this.posts = [];
+  }
+
+  async connectedCallback() {
+    super.connectedCallback();
+    const response = await client.query({
+      query: gql`query($parent: String!) {
+        children(parent: $parent) {
+          id,
+          title,
+          link,
+          filePath,
+          fileName,
+          template,
+          data {
+            date,
+            author
+          }
+        }
+      }`,
+      variables: {
+        parent: 'blog'
+      }
+    });
+
+    this.posts = response.data.children;
+  }
+
+  /* eslint-disable indent */
+  render() {
+    const { posts } = this;
+
+    return html`
+
+      <div class='container'>
+      
+        <entry></entry>
+
+        <div class="posts">
+          <h1>More Posts</h1>
+
+          <ul>
+            ${posts.map((post) => {
+              return html`
+                <li>
+                  <a href="${post.link}/" title="Click to read my ${post.title} blog post">
+                    ${post.title} posted: ${post.data.date}
+                  </a>
+                  <span>Author: ${post.data.author}</span>
+                </li>
+              `;
+            })}
+          </ul>
+        </div>
+
+      </div>
+    `;
+  }
+  /* eslint-enable */
+}
+
+customElements.define('page-template', BlogTemplate);
\ No newline at end of file
diff --git a/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js b/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js
index 25a37f89..b011efc2 100644
--- a/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js
+++ b/packages/cli/test/cases/build.data.graph/build.data.graph.spec.js
@@ -137,7 +137,7 @@ describe('Build Greenwood With: ', function() {
         expect(header.length).to.be.equal(1);
       });
 
-      it('should have a expected NavigationQuery output in the <header> tag', function() {
+      it('should have expected navigation links in the <header> tag tag when using NavigationQuery', function() {
         const listItems = dom.window.document.querySelectorAll('body header ul li');
         const link = listItems[0].querySelector('a');
 
@@ -147,7 +147,7 @@ describe('Build Greenwood With: ', function() {
         expect(link.innerHTML).to.contain('Blog');
       });
 
-      it('should have expected ChildrenQuery output in the <body> tag', function() {
+      it('should have expected blog posts links in the <body> tag when using ChildrenQuery', function() {
         const listItems = dom.window.document.querySelectorAll('body div.posts ul li');
         const linkItems = dom.window.document.querySelectorAll('body div.posts ul li a');
 
diff --git a/packages/cli/test/unit/data/mocks/graph.js b/packages/cli/test/unit/data/mocks/graph.js
index 3000c1dc..722ff14b 100644
--- a/packages/cli/test/unit/data/mocks/graph.js
+++ b/packages/cli/test/unit/data/mocks/graph.js
@@ -1,6 +1,7 @@
 const MOCK_GRAPH = {
   graph: [ 
-    { mdFile: './index.md',
+    { data: {},
+      mdFile: './index.md',
       label: '37b24fd9f163e7b',
       route: '/',
       template: 'home',
@@ -10,7 +11,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../index/index.js\'',
       title: 'Greenwood',
       meta: [Array] },
-    { mdFile: './about/community.md',
+    { data: {},
+      mdFile: './about/community.md',
       label: '94c871662fe47e5',
       route: '/about/community',
       template: 'page',
@@ -20,7 +22,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../about/community/community.js\'',
       title: 'About',
       meta: [Array] },
-    { mdFile: './about/features.md',
+    { data: {},
+      mdFile: './about/features.md',
       label: '856848d74d8428b',
       route: '/about/features',
       template: 'page',
@@ -30,7 +33,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../about/features/features.js\'',
       title: 'About',
       meta: [Array] },
-    { mdFile: './about/how-it-works.md',
+    { data: {},
+      mdFile: './about/how-it-works.md',
       label: 'f7f7b27e3c27d0c',
       route: '/about/how-it-works',
       template: 'page',
@@ -40,7 +44,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../about/how-it-works/how-it-works.js\'',
       title: 'About',
       meta: [Array] },
-    { mdFile: './about/goals.md',
+    { data: {},
+      mdFile: './about/goals.md',
       label: '9e1ec40126cbaa1',
       route: '/about/goals',
       template: 'page',
@@ -50,7 +55,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../about/goals/goals.js\'',
       title: 'About',
       meta: [Array] },
-    { mdFile: './about/index.md',
+    { data: {},
+      mdFile: './about/index.md',
       label: 'about',
       route: '/about/',
       template: 'page',
@@ -60,7 +66,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../about/index/index.js\'',
       title: 'About',
       meta: [Array] },
-    { mdFile: './docs/component-model.md',
+    { data: {},
+      mdFile: './docs/component-model.md',
       label: '41d241788aba406',
       route: '/docs/component-model',
       template: 'page',
@@ -70,7 +77,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/component-model/component-model.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './docs/configuration.md',
+    { data: {},
+      mdFile: './docs/configuration.md',
       label: 'b8027b72fecbdf6',
       route: '/docs/configuration',
       template: 'page',
@@ -80,7 +88,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/configuration/configuration.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './docs/index.md',
+    { data: {},
+      mdFile: './docs/index.md',
       label: '4ed3ee4d104185e',
       route: '/docs/',
       template: 'page',
@@ -90,7 +99,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/index/index.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './docs/front-matter.md',
+    { data: {},
+      mdFile: './docs/front-matter.md',
       label: '0441281c8197941',
       route: '/docs/front-matter',
       template: 'page',
@@ -100,7 +110,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/front-matter/front-matter.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './docs/layouts.md',
+    { data: {},
+      mdFile: './docs/layouts.md',
       label: 'ff00fdc5a5bb79e',
       route: '/docs/layouts',
       template: 'page',
@@ -110,7 +121,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/layouts/layouts.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './docs/css-and-images.md',
+    { data: {},
+      mdFile: './docs/css-and-images.md',
       label: '6ec677142cf4506',
       route: '/docs/css-and-images',
       template: 'page',
@@ -120,7 +132,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/css-and-images/css-and-images.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './docs/tech-stack.md',
+    { data: {},
+      mdFile: './docs/tech-stack.md',
       label: '5f2e96ac1d461f1',
       route: '/docs/tech-stack',
       template: 'page',
@@ -130,7 +143,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/tech-stack/tech-stack.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './getting-started/branding.md',
+    { data: { foo: 'bar1' },
+      mdFile: './getting-started/branding.md',
       label: '5436f1acd7a0297',
       route: '/getting-started/branding',
       template: 'page',
@@ -140,7 +154,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../getting-started/branding/branding.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './getting-started/creating-content.md',
+    { data: { foo: 'bar2' },
+      mdFile: './getting-started/creating-content.md',
       label: '878f45b3dea2a2e',
       route: '/getting-started/creating-content',
       template: 'page',
@@ -151,7 +166,8 @@ const MOCK_GRAPH = {
        '\'../getting-started/creating-content/creating-content.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './docs/markdown.md',
+    { data: {},
+      mdFile: './docs/markdown.md',
       label: 'd16333ec756763b',
       route: '/docs/markdown',
       template: 'page',
@@ -161,7 +177,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/markdown/markdown.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './getting-started/key-concepts.md',
+    { data: { foo: 'bar3' },
+      mdFile: './getting-started/key-concepts.md',
       label: '7135cdf1062f91e',
       route: '/getting-started/key-concepts',
       template: 'page',
@@ -171,7 +188,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../getting-started/key-concepts/key-concepts.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './getting-started/build-and-deploy.md',
+    { data: { foo: 'bar4' },
+      mdFile: './getting-started/build-and-deploy.md',
       label: '1abbe13654a8651',
       route: '/getting-started/build-and-deploy',
       template: 'page',
@@ -182,7 +200,8 @@ const MOCK_GRAPH = {
        '\'../getting-started/build-and-deploy/build-and-deploy.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './getting-started/index.md',
+    { data: { foo: 'bar5' },
+      mdFile: './getting-started/index.md',
       label: '0f09ddfde58fbc3',
       route: '/getting-started/',
       template: 'page',
@@ -192,7 +211,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../getting-started/index/index.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './getting-started/project-setup.md',
+    { data: { foo: 'bar6' },
+      mdFile: './getting-started/project-setup.md',
       label: 'd13f3b1a48b11ac',
       route: '/getting-started/project-setup',
       template: 'page',
@@ -202,7 +222,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../getting-started/project-setup/project-setup.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './getting-started/next-steps.md',
+    { data: { foo: 'bar7' },
+      mdFile: './getting-started/next-steps.md',
       label: 'e80510568562ced',
       route: '/getting-started/next-steps',
       template: 'page',
@@ -212,7 +233,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../getting-started/next-steps/next-steps.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './getting-started/quick-start.md',
+    { data: { foo: 'bar8' },
+      mdFile: './getting-started/quick-start.md',
       label: 'cf130a69289425d',
       route: '/getting-started/quick-start',
       template: 'page',
@@ -222,7 +244,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../getting-started/quick-start/quick-start.js\'',
       title: 'Getting Started',
       meta: [Array] },
-    { mdFile: './plugins/webpack.md',
+    { data: {},
+      mdFile: './plugins/webpack.md',
       label: '9d2b98c69ab0867',
       route: '/plugins/webpack',
       template: 'page',
@@ -232,7 +255,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../plugins/webpack/webpack.js\'',
       title: 'Plugins',
       meta: [Array] },
-    { mdFile: './plugins/composite-plugins.md',
+    { data: {},
+      mdFile: './plugins/composite-plugins.md',
       label: '8b231661fded283',
       route: '/plugins/composite-plugins',
       template: 'page',
@@ -242,7 +266,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../plugins/composite-plugins/composite-plugins.js\'',
       title: 'Plugins',
       meta: [Array] },
-    { mdFile: './plugins/index-hooks.md',
+    { data: {},
+      mdFile: './plugins/index-hooks.md',
       label: '272e40c3703d4f2',
       route: '/plugins/index-hooks',
       template: 'page',
@@ -252,7 +277,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../plugins/index-hooks/index-hooks.js\'',
       title: 'Plugins',
       meta: [Array] },
-    { mdFile: './docs/data.md',
+    { data: {},
+      mdFile: './docs/data.md',
       label: '5082835f0f7f7d7',
       route: '/docs/data',
       template: 'page',
@@ -262,7 +288,8 @@ const MOCK_GRAPH = {
       relativeExpectedPath: '\'../docs/data/data.js\'',
       title: 'Docs',
       meta: [Array] },
-    { mdFile: './plugins/index.md',
+    { data: {},
+      mdFile: './plugins/index.md',
       label: 'dd1ec2ef00cc386',
       route: '/plugins/',
       template: 'page',
diff --git a/packages/cli/test/unit/data/schema/graph.spec.js b/packages/cli/test/unit/data/schema/graph.spec.js
index 7fb0f4eb..81a4fd29 100644
--- a/packages/cli/test/unit/data/schema/graph.spec.js
+++ b/packages/cli/test/unit/data/schema/graph.spec.js
@@ -129,6 +129,12 @@ describe('Unit Test: Data', function() {
             expect(child.title).to.equal('Getting Started');
           });
         });
+
+        it('should have expected custom front matter data if it is set', function() {
+          children.forEach(function(child, index) {
+            expect(child.data.foo).to.equal(`bar${index + 1}`);
+          });
+        });
       });
     });