Gatsby React Minimum Viable Markdown Template / Component

Katie - Jun 18 '20 - - Dev Community

I discovered that a directory only needs two small files in it for the Gatsby static website generator to turn it into a functioning index.html with a body of <div>Hello world!</div>.

My next project is to build it up from an index.md Markdown file containing the message of "Hello world!" and have Gatsby inject it into index.html.

I'm still just shooting for some variation on this HTML for the contents of index.html:

<html>
    <head></head>
    <body>
        <div>Hello World</div>
    </body>
</html>

index.md looks different than I'm used to

In this case, I'm going to use an index.md file that doesn't have a Markdown-formatted "body;" it just has YAML-formatted "front matter" serving as data, like this:

---
message: Hello World
---

Again, note that I put "Hello World" in the "front matter" up top, not down in the body as Markdown like this:

---
---

Hello World

Since Topher Zimmerman's training on behalf of Magnolia CMS at Jamstack Conf 2 weeks ago, while further exploring Stackbit and TinaCMS, I've been learning that it's pretty common for Markdown files being used as data for static site generation with highly "WYSIWYG" drag-and-drop content management systems (CMSes) to have most data stored in the front matter of the .md file, not the body.

This lets you take advantage of YAML's ability to store "nested, ordered data" nicely.

That property CMSes help authors specify, with drag-and-drop interactivity, intent such as:

"Page sub-component B2 goes inside of page component B, after page sub-component B1, which is also inside of thing B.

"Page component B goes between page component A and page component C."

In fact, even if I wanted to let an author use Markdown to format "Hello World" into <ul><li>Hello</li><li>World</li></ul>, it's not unheard of to store that in the front matter too, like this:

---
message: |-
  * Hello
  * World
---

Instead of in the "body" of the .md file like this:

---
---

* Hello
* World

In Gatsby, it's traditional to put such an index.md file in a folder called pages that in turn is in a folder called src.

Furthermore, I have to include some "front matter" to specify what JavaScript "template" file I'm going to use to wrap the words "Hello World" between <div> & </div> tags, and it's conventional to simply call that front-matter property "template." I'm planning on naming the template "xyzzy," so I'll add template: xyzzy to the front matter.


Files

/src/pages/index.md

In short, my index.md file will have the following contents:

---
template: xyzzy
message: Hello World
---

/package.json

As in the 2-file Gatsby minimum viable build, I have to specify that certain Node packages like React and like Gatsby itself are essential to building the static site. I'll also specify two Gatsby plugins called gatsby-source-filesystem and gatsby-transformer-remark that I'll need:

{
    "name" : "netlify-gatsby-test-02",
    "description" : "Does this really work?",
    "version" : "0.0.2",
    "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"
    }
}

/gatsby-config.js

I need to "activate" the Gatsby plugins gatsby-source-filesystem and gatsby-transformer-remark with a file called gatsby-config.js:

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

/src/templates/xyzzy.js

Gatsby leans heavily upon a JavaScript library called React, and specifically upon its notion of "React components" (not to be confused with the contents of /src/components/ in this project's folder structure, although that most certainly is a folder full of files defining React components -- it's just that /src/templates is also full of React components).

  • The best way to learn what I mean by "React component" is to read the first 4 sections of the official React documentation. They're well-written in tutorial style, so don't worry -- it's not a dry read.

Whenever Gatsby is told to use a given React component such as xyzzy.js to render a given "page" of a web site that it's decided needs rendering (I'll soon write code in a file called gastby-node.js telling xyzzy.js about index.md), it passes information into the Xyzzy component as pageContext.

  • If you define your React component as a function, name its parameter pageContext.
  • If you defined your React component as a class that extends React.Component, you'll automatically have this information accessible within your class as this.props.pageContext.

I'll dynamically fetch the text "Hello World" from /src/pages/index.md as a detail of pageContext called pageContext.frontmatter.message (or, if I had been using classes, this.props.pageContext.frontmatter.message).

Note that the "frontmatter" property of pageContext doesn't exist yet. I'll soon write code in a file called gastby-node.js that makes Gatsby include frontmatter in pageContext.

For now, just trust that {pageContext.frontmatter.message} is the equivalent of Hello World because that's what's in the "front matter" of index.md.

Variation 1 (standalone template)

import React from "react"

export default function Xyzzy({ pageContext }) {
  return (
    <div>
      {pageContext.frontmatter.message}
    </div>
  )
}

If I don't have anything too complicated to put into the home page of my web site, I can write DIV tags directly into xyzzy.js surrounding {pageContext.frontmatter.message}.

Note that <div>{pageContext.frontmatter.message}</div> technically isn't HTML -- it's JSX / a React element.

Variation 2 (leveraging another component)

import React from "react"

import BasicDiv from '../components/basicDiv.js';

export default function Xyzzy({ pageContext }) {
  return (
    <BasicDiv messageToDisplay={pageContext.frontmatter.message} />
  )
}

If I'm feeling fancy and prefer to delegate the building of the JSX React element <div>Hello World</div> to a different React Component (for code modularity and reusability), I can add an extra file /src/components/basicDiv.js to my directory structure, "import" it into xyzzy.js, and execute it from within xyzzy.js's JSX as .

To pass BasicDiv a parameter specifying "Hello World" as seen in index.md, I'll make that information an attribute of the call to <BasicDiv />.


/src/components/basicDiv.js

Note that I only need this file when using "variation 2" of /src/templates/xyzzy.js.

import React from "react"

export default function BasicDiv(props) {
  return (
    <div>
      {props.messageToDisplay}
    </div>
  )
}

The BasicDiv React component is never used directly by Gatsby in the building of a "page" from data such as the contents of index.md, so when defining it as a function, its parameter name should be a more traditional props rather than pageContext.

Because I know I summoned it elsewhere with a message parameter formatted <BasicDiv message={some-data-here} />, I can access the contents of message as props.message (or, if I'd defined BasicDiv as a class, as this.props.message).

Since BasicDiv is never passed any sort of parameter named pageContext by Gatsby, the HTML-like JSX code I use to render my DIV is, instead, <div>{props.message}</div> or <div>{this.props.message}</div>.


/gatsby-node.js

Finally, I come to the beastly file that teaches Gatsby how to put everything together: 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, actions }) => {
  const { createPage } = actions
  const queryResult = await graphql(`
    query {
      allMarkdownRemark {
          edges {
            node {
              fields {
                suggestedURLSuffix
              },
              frontmatter {
                template,
                message
              }
            }
          }
      }
    }
  `)
  nodes = queryResult.data.allMarkdownRemark.edges
  nodes.forEach(({ node }) => {
    createPage({
      path: node.fields.suggestedURLSuffix,
      component: path.resolve(`./src/templates/${node.frontmatter.template}.js`),
      context: {
        frontmatter: node.frontmatter,
      },
    })
  })
};

Overriding createPages

Earlier, I said that I would need to teach Gatsby to pass details from the "front matter" of index.md to xyzzy.js as a frontmatter sub-property of a Gatsby concept called pageContext.

I do this by overriding Gatsby's definition of a Gatsby function called createPages() within a file called gatsby-node.js that I'll place in the root directory of my folder structure.

The first thing I do is tell Gatsby to make a GraphQL query against its entire self-inventory of stuff it knows about and likes to call "nodes" (not to be confused with Node for which Gatsby is a package), filtering to only return the ones that seem to be Markdown-formatted files, and save the resulting JavaScript object into a variable I decided to call queryResult.

The part of queryResult that I'm interested in is an array of "node" objects accessible through queryResult.data.allMarkdownRemark.edges -- I'll set that array aside into a variabled called nodes.

(In my case, there's only one object in the nodes array: the one representing the contents of the file /src/pages/index.md.)

For each loop over a node in nodes, I'll call a Gatsby function actions.createPage().

  1. I'll check what the node's suggestedURLSuffix is and tell Gatsby to make the web page it renders available at https://mysite.com/whatever-is-in-that-url-suffix by setting path in the JavaScript object I pass createPage() to node.fields.suggestedURLSuffix.
    • More in a minute on where the value of suggestedURLSuffix comes from.
  2. I'll also look through the front-matter properties of my Markdown-formatted file for one called template and concatenate it with other data to indicate a file path in my folder structure where Gatsby can find the definition of the React component that I'd like to use for transforming the Markdown-formatted file into HTML on my actual web site. In the case of index.md, there's a property of template: xyzzy, and so setting component to path.resolve(`./src/templates/$xyzzy.js`) will ensure that index.md is associated with xyzzy.js.
  3. Finally, I will say that I'd like the node.frontmatter details returned by my GraphQL query to be passed along to xyzzy.js as part of pageContext by including them in the context property of the JavaScript object I pass createPage(). context itself takes a JavaScript object as a value, into which I put just one key-value pair: frontmatter as the key, and node.frontmatter as the value.

Overriding onCreateNode

I promised to explain the origin of suggestedURLSuffix.

I define suggestedURLSuffix by overriding Gatsby's definition of a Gatsby function called onCreateNode, also within the gatsby-node.js file.

Basically, I can use onCreateNode() to trick GraphQL into thinking that the "nodes" it's querying have properties they don't inherently have -- like suggestedURLSuffix.

In my case, I populate the value of suggestedURLSuffix with output from a call to the gatsby-source-filesystem plugin's createFilePath() function.


Output

That's it! Just 5 files (or 6, to demonstrate breaking a BasicDiv function out of Xyzzy) and you're up and running with Markdown in Gatsby.

The resulting page has the following HTML:

<!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"/>
        <meta name="generator" content="Gatsby 2.23.3"/>
        <link as="script" rel="preload" href="/component---src-templates-xyzzy-js-61a4d70344455ce4bf22.js"/>
        <link as="script" rel="preload" href="/framework-4d07bacc3808af3f4337.js"/>
        <link as="script" rel="preload" href="/app-3cfcd55108b187700e99.js"/>
        <link as="script" rel="preload" href="/webpack-runtime-2f540f584be2422b9aa4.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>Hello World</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-3cfcd55108b187700e99.js"],"component---src-templates-xyzzy-js":["/component---src-templates-xyzzy-js-61a4d70344455ce4bf22.js"]};/*]]>*/</script><script src="/webpack-runtime-2f540f584be2422b9aa4.js" async=""></script><script src="/app-3cfcd55108b187700e99.js" async=""></script><script src="/framework-4d07bacc3808af3f4337.js" async=""></script><script src="/component---src-templates-xyzzy-js-61a4d70344455ce4bf22.js" async=""></script>
    </body>
</html>

Helpful links

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