Gatsby React WSYIWYG CMS-Friendly Markdown

Katie - Jun 22 '20 - - Dev Community

Come see how I built a drag-and-drop WSYIWYG CMS-ready web page with Gatsy and React.


I mentioned before that content management systems (CMSes) for static site generators have gotten pretty fancy.

Magnolia CMS, Stackbit, and TinaCMS all allow non-technical content authors drag and drop visual components around web pages in a semi-"what you see is what you get" (WYSIWYG) editing experience that is probably as close to Squarespace as the Jamstack comes right now.

All these CMSes rely upon pages being built from data sources that can be treated as an outermost object / dictionary / map with various values (including more objects or lists / arrays) assigned to their keys.


End goal HTML

Let's say I wanted a home page that looks like this:

Screenshot of my home page before moving contents

 

Example HTML might be:

<html>
  <head></head>
  <body>
    <div class="hello-layout-wrapper">
      <div class="hello-layout-header">
        Header placeholder
      </div>
      <div class="hello-layout-main">
        <div class="pink-div">
          I did it!
        </div>
        <div class="task-list">
          <div class="task task-odd">
            <b></b> <b>eat</b> <i>well</i>
          </div>
          <div class="task task-even">
            <b>X</b> <b>sleep</b> <i>soundly</i>
          </div>
          <div class="task task-odd">
            <b></b> <b>jump</b> <i>high</i>
          </div>
          <div class="task task-even">
            <b></b> <b>write</b>
          </div>
          <div class="task task-odd">
            <b></b> <b>hydrate</b> <i>regularly</i>
          </div>
        </div> <!-- .task-list -->
        <div class="blue-div">
          Hello World
        </div>
      </div> <!-- .hello-layout-main -->
      <div class="hello-layout-footer">
        Footer placeholder
      </div>
    </div> <!-- .hello-layout-wrapper -->
  </body>
</html>

However, I also want it to be easy for an author to edit and re-order, and even add sections so that they look like this:

Screenshot of my home page after moving contents


Page data structure

Design

Imagine hand-writing the following bulleted list in pencil, coloring it with markers, and cutting the paper horizontally at "section" breaks.

  • template: xyzzy
  • sections:
    • (this is 1 item in a list of sections)
      • type: SectionPink
      • say: I did it!
    • (this is 1 item in a list of sections)
      • type: SectionTaskList
      • accomplishments:
        • (this is 1 item in a list of accomplishments)
          • task: eat
          • done: true
          • how: well
        • (this is 1 item in a list of accomplishments)
          • task: sleep
          • done: false
          • how: soundly
        • (this is 1 item in a list of accomplishments)
          • task: jump
          • done: true
          • how: high
        • (this is 1 item in a list of accomplishments)
          • task: write
          • done: true
        • (this is 1 item in a list of accomplishments)
          • task: hydrate
          • done: true
          • how: regularly
    • (this is 1 item in a list of sections)
      • type: SectionBlue
      • mention: Hello World

This approach to structuring the data that I'll let my author edit makes it quite easy to imagine moving strips of paper up and down, rearranging them, right?

It'd also be pretty easy to add extra strips.

It'd be pretty easy to erase phrases and rewrite them as well.


Computerization

Any plain-text punctuation standard optimized for storing nested, ordered data in a way that is both human-readable and computer-readable would be a great way to digitize this craft-paper project.

JSON

Here's what it might look like in the JSON punctuation standard:

{
   "template": "xyzzy",
   "sections": [
      {
         "type": "SectionPink",
         "say": "I did it!"
      },
      {
         "type": "SectionTaskList",
         "accomplishments": [
            {
               "task": "eat",
               "done": true,
               "how": "well"
            },
            {
               "task": "sleep",
               "done": false,
               "how": "soundly"
            },
            {
               "task": "jump",
               "done": true,
               "how": "high"
            },
            {
               "task": "write",
               "done": true
            },
            {
               "task": "hydrate",
               "done": true,
               "how": "regularly"
            }
         ]
      },
      {
         "type": "SectionBlue",
         "mention": "Hello World"
      }
   ]
}

YAML

Here it is in the YAML punctuation standard, which is commonly used to store data as "front matter" at the top of "Markdown"-formatted files (between a pair of --- lines) in the world of static website generation:

template: xyzzy
sections:
  - type: SectionPink
    say: I did it!
  - type: SectionTaskList
    accomplishments:
      - task: eat
        done: true
        how: well
      - task: sleep
        done: false
        how: soundly
      - task: jump
        done: true
        how: high
      - task: write
        done: true
      - task: hydrate
        done: true
        how: regularly
  - type: SectionBlue
    mention: Hello World

Editing

This data might look pretty complicated for a non-technical author to edit by hand, but remember that they won't have to.

A CMS will write these data files for them based on the actions they take within the CMS's editor.


Files

I'm up to 13 files now from my original 2.

I think that makes it time to doodle out one of those little "folder structure" diagrams. You can also see the code on GitHub here.

.
├── src
│   ├── components
│   │   ├── indexSectionComponents.js
│   │   ├── layoutHello.js
│   │   ├── sectionBlue.js
│   │   ├── sectionPink.js
│   │   ├── sectionTaskList.js
│   │   └── task.js
│   ├── css
│   │   └── global.css
│   ├── pages
│   │   └── index.md
│   └── templates
│       └── xyzzy.js
├── gatsby-browser.js
├── gatsby-config.js
├── gatsby-node.js
└── package.json

File: /src/pages/index.md

Before authoring

---
template: xyzzy
sections:
  - type: SectionPink
    say: I did it!
  - type: SectionTaskList
    accomplishments:
      - task: eat
        done: true
        how: well
      - task: sleep
        done: false
        how: soundly
      - task: jump
        done: true
        how: high
      - task: write
        done: true
      - task: hydrate
        done: true
        how: regularly
  - type: SectionBlue
    mention: Hello World
---

After authoring

---
template: xyzzy
sections:
  - type: SectionBlue
    mention: Hello beautiful world.
  - type: SectionPink
    say: Wow, I did it!
  - type: SectionTaskList
    accomplishments:
      - task: stretch
        done: true
        how: gracefully
      - task: eat
        done: true
        how: well
      - task: write
        done: false
      - task: sleep
        done: false
        how: soundly
      - task: jump
        done: false
        how: high
      - task: hydrate
        done: true
        how: regularly
  - type: SectionPink
    say: I'm proud of me.
---

File: /src/css/global.css

.pink-div {
    background-color: #d81b60;
    color: black;
}

.blue-div {
    background-color: #1e88e5;
    color: white;
}

.task.task-odd {
    background-color: #ffc107;
    color: black;
}

.task.task-even {
    background-color: #004d40;
    color: white;
}

Note that I color the individual tasks/accomplishments by their "odd" / "even" placement, not by anything inherent to their contents.


File: /gatsby-browser.js

require('./src/css/global.css')

This file makes Gatsby actually pay attention to my CSS.


File: /package.json

{
    "name" : "netlify-gatsby-test-04",
    "description" : "Does this really work?",
    "version" : "0.0.4",
    "scripts" : {
        "develop": "gatsby develop",
        "start": "npm run develop",
        "build": "gatsby build",
        "serve": "gatsby serve"
    },
    "dependencies" : {
        "gatsby": ">=2.22.15",
        "gatsby-source-filesystem": ">=2.3.11",
        "gatsby-transformer-remark": ">=2.8.15",
        "react": ">=16.12.0",
        "react-dom": ">=16.12.0"
    }
}

This didn't change from the previous exercise.


File: /gatsby-config.js

module.exports = {
    plugins: [
        {
            resolve: `gatsby-source-filesystem`,
            options: {
                name: `pages`,
                path: `${__dirname}/src/pages`,
            },
        },
        `gatsby-transformer-remark`
    ]
};

This didn't change from the previous exercise.


File: /gatsby-node.js

const path = require(`path`)
const { createFilePath } = require(`gatsby-source-filesystem`)

exports.onCreateNode = ({ node, getNode, actions }) => {
  const { createNodeField } = actions
  if (node.internal.type === `MarkdownRemark`) {
    const urlSuffixIdea = createFilePath({ node, getNode, basePath: `pages` })
    createNodeField({
      node,
      name: `suggestedURLSuffix`,
      value: urlSuffixIdea,
    })
  }
}

exports.createPages = async ({ graphql, getNode, actions }) => {
  const { createPage } = actions
  const queryResult = await graphql(`
    query {
      allMarkdownRemark {
          edges {
            node {
              id,
              fields {
                suggestedURLSuffix
              }
            }
          }
      }
    }
  `)
  nodes = queryResult.data.allMarkdownRemark.edges
  nodes.forEach(({ node }) => {
    const freshNode = getNode(node.id);
    createPage({
      path: node.fields.suggestedURLSuffix,
      component: path.resolve(`./src/templates/${freshNode.frontmatter.template}.js`),
      context: {
        frontmatter: freshNode.frontmatter,
      },
    })
  })
};

This changed very little from the previous exercise.

I made a slight alteration to the way I override onCreateNode() to make it more flexible about the "front matter" properties it encounters in my .md file(s) -- be sure to take a look at what I do with a variable I call freshNode (thanks, Stackbit template authors, for the example).


File: /src/templates/xyzzy.js

import React from "react"

import LayoutHello from '../components/layoutHello.js';
import sectionComponentTypeList from '../components/indexSectionComponents.js';

export default function Xyzzy({ pageContext }) {
  const sections = pageContext.frontmatter.sections;
  const SectionComponents = sections.map((section) => {
    let sectionType = section.type;
    let Component = sectionComponentTypeList[sectionType];
    return (
      <Component section={section} />
    )
  });
  return (
    <LayoutHello>
      <div className='xyzzy'>
        {SectionComponents}
      </div>
    </LayoutHello>
  )
}

The Xyzzy React component that I use as a "template" for index.md has changed a bit.

Instead of delegating content rendering to a BasicDiv React component, I delegate it to an array of React components that I refer to locally as SectionComponents.

I never actually wrote a file definining anything called SectionComponents (or Component, for that matter, which also appears within a JSX tag in my code).

My ability to dynamically choose from among the React components I wrote called SectionBlue, SectionPink, and SectionTaskList, depending on what I find within the type sub-property of a given item within sections in my index.md front matter, depends upon some trickery within a file at /src/components/indexSectionComponents.js.

I also wrapped things in a "layout"-typed Gatsby component (a component meant to wrap around other content instead of being placed inside it) named LayoutHello just to see how it works.


Let's move on to the rest of my components. They're all thrown into a mess under /src/components/*.js, even though they do different things and serve at different "levels" of my page rendering. This seems to be pretty standard in small Gatsby/React projects.

File: /src/components/layoutHello.js

import React from "react"

export default function LayoutHello({ children }) {
  return (
    <div className="hello-layout-wrapper">
      <div className="hello-layout-header">Header placeholder</div>
      <div className="hello-layout-main">
        { children }
      </div>
      <div className="hello-layout-footer">Footer placeholder</div>
    </div>
  )
}

LayoutHello's magic is in naming its input parameter { children }. That turns a React component into a "layout component" in Gatsby.

I used it to put a header & footer into my home page.


File: /src/components/sectionBlue.js

import React from "react"

export default function SectionBlue(props) {
  return (
    <div className="blue-div">
      {props.section.mention}
    </div>
  )
}

SectionBlue React components trust their calling code to send them object-typed details attached to an attribute named section, and to make sure that one of the properties of that object is named mention.

Lucky me I didn't make any typos in index.md's front matter, like forgetting mention:

- type: SectionBlue
  mention: Hello beautiful world.

Of course, the configuration file for a good CMS can help me specify mention as a required field for SectionBlue sections. That would guarantee that no author ever forgets to write content for mention.

Also, I'm speaking a bit loosely about index.md as if SectionBlue knew anything about it. It doesn't. index.md's contents are converted to Javascript objects by createPages() in gatsby-node.js long before SectionBlue becomes involved in rendering HTML for my web site.


File: /src/components/sectionPink.js

import React from "react"

export default function SectionPink(props) {
  return (
    <div className="pink-div">
      {props.section.say}
    </div>
  )
}

SectionPink React components trust their calling code to send them object-typed details attached to an attribute named section, and to make sure that one of the properties of that object is named say.


File: /src/components/task.js

import React from "react"

export default function Task(props) {
  let checkboxSymbol;
  if (props.taskDetail.done) {
    checkboxSymbol = '';
  } else {
    checkboxSymbol = 'X';
  }
  const classes = `task ${props.alternatingClassName}`
  return (
    <div className={classes}>
      <b>{checkboxSymbol}</b>
      {' '}
      <b>{props.taskDetail.task}</b>
      { props.taskDetail.how && 
        <i>{' '}{props.taskDetail.how}</i>
      }
    </div>
  )
}

Task React components trust their calling code to send them a simple plaintext value in an attribute named alternatingClassName, as well as object-typed details attached to an attribute named taskDetail, and to make sure that that object in turn has task, done, and possibly how properties of its own.


File: /src/components/sectionTaskList.js

import React from "react"

import Task from './task.js';

export default function SectionTaskList(props) {
  const alternatingClassNames = ['task-odd', 'task-even']; // Per https://stackoverflow.com/a/45467474
  const tasks = props.section.accomplishments;
  const taskItems = tasks.map((taskToDo, index) =>
    <Task taskDetail={taskToDo} alternatingClassName={alternatingClassNames[index % alternatingClassNames.length]} />
  );
  return (
    <div className="task-list">    
      {taskItems}
    </div>
  )
}

SectionTaskList React components trust their calling code to send them object-typed details attached to an attribute named section, and to make sure that one of the properties of that object is an array/list named accomplishments.

For each item found in accomplishments, SectionTaskList summons the Task React component, passing it the details of the item in question as taskDetail, and also passing it the phrase task-odd or task-even, depending on where the item was found in accomplishments.


File: /src/components/indexSectionComponents.js

import SectionBlue from  './sectionBlue.js';
import SectionPink from  './sectionPink.js';
import SectionTaskList from  './sectionTaskList.js';

export default {
    SectionBlue,
    SectionPink,
    SectionTaskList
};

Finally, the only goal in life of indexSectionComponents.js is to make it easier for Layout to "look up" an appropriate React component from among SectionBlue, SectionPink, and SectionTaskList based on the data it finds in a Markdown-formatted file's front matter.


Output

HTML

Before authoring

The resulting page has the following HTML before my author changes any data:

<!DOCTYPE html>
<html>
    <head>
        <meta charSet="utf-8"/>
        <meta http-equiv="x-ua-compatible" content="ie=edge"/>
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
        <style data-href="/styles.ebc7626ff23287782a05.css">.pink-div{background-color:#d81b60;color:#000}.blue-div{background-color:#1e88e5;color:#fff}.task.task-odd{background-color:#ffc107;color:#000}.task.task-even{background-color:#004d40;color:#fff}</style>
        <meta name="generator" content="Gatsby 2.23.3"/>
        <link as="script" rel="preload" href="/component---src-templates-xyzzy-js-ca0f5fd88a2ff3c9c2f8.js"/>
        <link as="script" rel="preload" href="/styles-dd3841a4888192e20843.js"/>
        <link as="script" rel="preload" href="/app-d3bc4de7e5ad640ea7fa.js"/>
        <link as="script" rel="preload" href="/framework-4d07bacc3808af3f4337.js"/>
        <link as="script" rel="preload" href="/webpack-runtime-7db46c7381bf829cfc7b.js"/>
        <link as="fetch" rel="preload" href="/page-data/index/page-data.json" crossorigin="anonymous"/>
        <link as="fetch" rel="preload" href="/page-data/app-data.json" crossorigin="anonymous"/>
    </head>
    <body>
        <div id="___gatsby">
            <div style="outline:none" tabindex="-1" id="gatsby-focus-wrapper">
                <div class="hello-layout-wrapper">
                    <div class="hello-layout-header">Header placeholder</div>
                    <div class="hello-layout-main">
                        <div class="xyzzy">
                            <div class="pink-div">I did it!</div>
                            <div class="task-list">
                                <div class="task task-odd">
                                    <b></b> <b>eat</b>
                                    <i>
                                        <!-- -->well
                                    </i>
                                </div>
                                <div class="task task-even">
                                    <b>X</b> <b>sleep</b>
                                    <i>
                                        <!-- -->soundly
                                    </i>
                                </div>
                                <div class="task task-odd">
                                    <b></b> <b>jump</b>
                                    <i>
                                        <!-- -->high
                                    </i>
                                </div>
                                <div class="task task-even"><b></b> <b>write</b></div>
                                <div class="task task-odd">
                                    <b></b> <b>hydrate</b>
                                    <i>
                                        <!-- -->regularly
                                    </i>
                                </div>
                            </div>
                            <div class="blue-div">Hello World</div>
                        </div>
                    </div>
                    <div class="hello-layout-footer">Footer placeholder</div>
                </div>
            </div>
            <div id="gatsby-announcer" style="position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0" aria-live="assertive" aria-atomic="true"></div>
        </div>
        <script id="gatsby-script-loader">/*<![CDATA[*/window.pagePath="/";/*]]>*/</script><script id="gatsby-chunk-mapping">/*<![CDATA[*/window.___chunkMapping={"app":["/app-d3bc4de7e5ad640ea7fa.js"],"component---src-templates-xyzzy-js":["/component---src-templates-xyzzy-js-ca0f5fd88a2ff3c9c2f8.js"]};/*]]>*/</script><script src="/webpack-runtime-7db46c7381bf829cfc7b.js" async=""></script><script src="/framework-4d07bacc3808af3f4337.js" async=""></script><script src="/app-d3bc4de7e5ad640ea7fa.js" async=""></script><script src="/styles-dd3841a4888192e20843.js" async=""></script><script src="/component---src-templates-xyzzy-js-ca0f5fd88a2ff3c9c2f8.js" async=""></script>
    </body>
</html>

After authoring

The resulting page has the following HTML after my author changes some data:

<!DOCTYPE html>
<html>
    <head>
        <meta charSet="utf-8"/>
        <meta http-equiv="x-ua-compatible" content="ie=edge"/>
        <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"/>
        <style data-href="/styles.ebc7626ff23287782a05.css">.pink-div{background-color:#d81b60;color:#000}.blue-div{background-color:#1e88e5;color:#fff}.task.task-odd{background-color:#ffc107;color:#000}.task.task-even{background-color:#004d40;color:#fff}</style>
        <meta name="generator" content="Gatsby 2.23.3"/>
        <link as="script" rel="preload" href="/component---src-templates-xyzzy-js-ca0f5fd88a2ff3c9c2f8.js"/>
        <link as="script" rel="preload" href="/styles-dd3841a4888192e20843.js"/>
        <link as="script" rel="preload" href="/app-d3bc4de7e5ad640ea7fa.js"/>
        <link as="script" rel="preload" href="/framework-4d07bacc3808af3f4337.js"/>
        <link as="script" rel="preload" href="/webpack-runtime-7db46c7381bf829cfc7b.js"/>
        <link as="fetch" rel="preload" href="/page-data/index/page-data.json" crossorigin="anonymous"/>
        <link as="fetch" rel="preload" href="/page-data/app-data.json" crossorigin="anonymous"/>
    </head>
    <body>
        <div id="___gatsby">
            <div style="outline:none" tabindex="-1" id="gatsby-focus-wrapper">
                <div class="hello-layout-wrapper">
                    <div class="hello-layout-header">Header placeholder</div>
                    <div class="hello-layout-main">
                        <div class="xyzzy">
                            <div class="blue-div">Hello beautiful world.</div>
                            <div class="pink-div">Wow, I did it!</div>
                            <div class="task-list">
                                <div class="task task-odd">
                                    <b></b> <b>stretch</b>
                                    <i>
                                        <!-- -->gracefully
                                    </i>
                                </div>
                                <div class="task task-even">
                                    <b></b> <b>eat</b>
                                    <i>
                                        <!-- -->well
                                    </i>
                                </div>
                                <div class="task task-odd"><b>X</b> <b>write</b></div>
                                <div class="task task-even">
                                    <b>X</b> <b>sleep</b>
                                    <i>
                                        <!-- -->soundly
                                    </i>
                                </div>
                                <div class="task task-odd">
                                    <b>X</b> <b>jump</b>
                                    <i>
                                        <!-- -->high
                                    </i>
                                </div>
                                <div class="task task-even">
                                    <b></b> <b>hydrate</b>
                                    <i>
                                        <!-- -->regularly
                                    </i>
                                </div>
                            </div>
                            <div class="pink-div">I&#x27;m proud of me.</div>
                        </div>
                    </div>
                    <div class="hello-layout-footer">Footer placeholder</div>
                </div>
            </div>
            <div id="gatsby-announcer" style="position:absolute;top:0;width:1px;height:1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);white-space:nowrap;border:0" aria-live="assertive" aria-atomic="true"></div>
        </div>
        <script id="gatsby-script-loader">/*<![CDATA[*/window.pagePath="/";/*]]>*/</script><script id="gatsby-chunk-mapping">/*<![CDATA[*/window.___chunkMapping={"app":["/app-d3bc4de7e5ad640ea7fa.js"],"component---src-templates-xyzzy-js":["/component---src-templates-xyzzy-js-ca0f5fd88a2ff3c9c2f8.js"]};/*]]>*/</script><script src="/webpack-runtime-7db46c7381bf829cfc7b.js" async=""></script><script src="/framework-4d07bacc3808af3f4337.js" async=""></script><script src="/app-d3bc4de7e5ad640ea7fa.js" async=""></script><script src="/styles-dd3841a4888192e20843.js" async=""></script><script src="/component---src-templates-xyzzy-js-ca0f5fd88a2ff3c9c2f8.js" async=""></script>
    </body>
</html>

Visual

Before authoring

The finished page looks roughly like this before authoring:

Screenshot of my home page before moving contents

After authoring

And like this afterwards:

Screenshot of my home page after moving contents


Next steps

Pretty neat.

Next project: setting up a CMS editing experience for index.md

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .