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.
Gatsby React Minimum Viable Markdown Template / Component
Katie ・ Jun 18 ・ 9 min read
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:
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:
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!
-
type:
-
(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
-
task:
-
(this is 1 item in a list of accomplishments)
-
task:
sleep
-
done:
false
-
how:
soundly
-
task:
-
(this is 1 item in a list of accomplishments)
-
task:
jump
-
done:
true
-
how:
high
-
task:
-
(this is 1 item in a list of accomplishments)
-
task:
write
-
done:
true
-
task:
-
(this is 1 item in a list of accomplishments)
-
task:
hydrate
-
done:
true
-
how:
regularly
-
task:
-
(this is 1 item in a list of accomplishments)
-
type:
-
(this is 1 item in a list of sections)
-
type:
SectionBlue
-
mention:
Hello World
-
type:
-
(this is 1 item in a list of sections)
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.
Intro to XML and JSON #2: Data's Shape
Katie ・ Mar 25 '19 ・ 8 min read
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'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:
After authoring
And like this afterwards:
Next steps
Pretty neat.
Next project: setting up a CMS editing experience for index.md
Top comments (0)