Gatsby And Cosmic JS Tutorial For Building Documentation

Gstby Documentation

Gatsby is an easy-to-use framework for generating static website files.  It comes bundled with all sorts of hotness, like React JS for building web components. And GraphQL for handling our component state without the need to configure something like Redux to handle external data

Cosmic JS will handle our publishing and data storage.  It’s easy to set up and easy to implement for apps like this. Yet scalable enough to handle more complex projects across larger teams.  We will use this to create and store our documentation content.  This will us allowing us to focus on how our users interact with our app and let Cosmic JS do all the heavy lifting.

Is that all?

Well no… we are going to convert our docs from markdown to HTML since that’s what web Browsers like.  To do this we are going to use a package called Showdown. that can handle parsing and converting markdown to and from HTML.

Any Requirements?

You will need to have access to a terminal, a Cosmic JS account with a bucket, and a documentation object. The latest version of Node is installed in order to install the necessary software to make this app work.  I’m going to be using yarn to run my build scripts but you can npm if you like.  Remember to choose npm or yarn and stick with it as things can get a little hairy when it’s time to deploy.

Let’s Build!!

1.1 – Setting up our Development Environment

To get started we will want to install Gatsby and install our dependencies.  Easy peasy. Gatsby uses a handy command line interface (CLI) to build our initial project files.  First, we will want to install the CLI by installing it globally with npm:

$ npm install -g gatsby-cli

This gives us access to the gatsby command and allows us to initialize our project.  Run the following script to create a new directory filled with a project template:

$ gatsby new gatsby-docs

Wait for a second to allow the script to complete and you will notice a new directory created called gatsby-docs.   Let’s see what’s inside by changing directories:

$ cd gatsby-docs

Much of this will look familiar if are used to creating Node applications but some of this will be a little new. You should be able to get a development server up and running by executing the start script:

$ yarn start

After a second you should see a success prompt letting you know that everything has compiled properly and your app is live.

Now you can open up your browser pointing to localhost:8000 and see the compiled output.

Congrats! You have set up a working Gatsby site. But before we dig into what is going on under the covers let’s install the rest of our dependencies that will power our app:

$ yarn add cosmicjs showdown highlight.js 
 	dotenv node-sass gatsby-plugin-sass gatsby-source
 	-graphql

Whoa… That’s a lot of newly installed packages, but each of these is super useful I swear.

  • cosmicjs will be used to add new content to our app.
  • showdown is the text parser I mentioned that will handle markdown and HTML conversion.
  • highlight.js is going to handle our syntax highlighting inside of our converted markdown text.
  • dotenv is an environment variable package that will make sure our sensitive tokens and/or runtime environment is configured from a .env file
  • node-sass and the gatsby-plugin-sass packages will allow using .scss files to style our components.
  • gatsby-source-graphql will allow us to leverage GraphQL queries on external data (ie – use the Cosmic JS GraphQL api)

With all of that business out of the way. We can look at our directory and configure our Gatsby source code to run properly.

2.0 – Configuring Gatsby

Now we can dig into our directory and make sure Gatsby is configured properly. For using the technologies that will scalably and sensibly power our app.

The first file we will want to look into is gatsby-config.js.  This file is used to configure high-level plugins, that allow source code to be bundled properly when static files are built.  It also contains a bit of metadata that describes our site to users and can be queried in our React Components.

Here we will add our newly installed plugins to the default configuration you see before you.  First, we just need to add gatsby-plugin-sass to the plugins list, allowing us to import sass files and leverage sass to write sensible styling specs for each component.

Next up we will add an object to the end of our plugins list  gatsby-source-graphql that will configure our external GraphQL API endpoint to allow us to fetch data from Cosmic JS.  Here’s how things should look:

module.exports = {
  siteMetadata: {
    title: `Gatsby Docs`,
    description: `Create and View Minimalistic Documentation, powered by GatsbyJs and CosmicJS`,
    author: `@jacobknaack`,
  },
  plugins: [
    `gatsby-plugin-eslint`,
    `gatsby-plugin-react-helmet`,
    `gatsby-transformer-sharp`,
    `gatsby-plugin-sharp`,
    `gatsby-plugin-sass`,
    {
      resolve: `gatsby-plugin-manifest`,
      options: {
        name: `gatsby-docs`,
        short_name: `docs`,
        start_url: `/`,
        display: `minimal-ui`,
        icon: `src/images/gatsby-icon.png`, // This path is relative to the root of the site.
      },
    },
    {
      resolve: `gatsby-source-graphql`,
      options: {
        url: `https://graphql.cosmicjs.com/v1`,
        fieldName: `docs`,
        typeName: `Doc`,
        refetchInterval: 10,
      },
    },
    // this (optional) plugin enables Progressive Web App + Offline functionality
    // To learn more, visit: https://gatsby.app/offline
    // 'gatsby-plugin-offline',
  ],
}

Now we are set to make GraphQL queries to the Cosmic JS GraphQL API!  Next, Let’s talk for a second about Gatsby and how things are going to break down.

2.1 Building our app with Gatsby

I’ve mentioned that Gatsby is a static site generator, but what does that mean?  Gatsby takes all the fancy code we create and produces static files that are pre-configured using the config files we specify.  By doing so we get increased performance for sites that may have lots of images, data to be fetched, and other assets that can slow down web applications.

Let’s now get some source code created.  Our site is going to use just two ‘Pages’, one that will serve as a home page to list the documentation we’ve created, and one for viewing a piece of documentation.  But to fetch the content that we are going to display, we are going to use GraphQL, which we have recently configured.  We will need to add some variables to our gatsby-node.js file in order to allow our static files to have the necessary parameters to make API calls.

Create a .env file and add your Cosmic JS environment variables

In your Cosmic JS Bucket > Basic Settings menu you will see fields for a bucket-slug and read and write keys down at the bottom.  Copy all three of these things and add them to a .env file.

At your project root, type into your terminal:

$ touch .env

Now Create three lines:

COSMIC_BUCKET=your-bucket-slug
COSMIC_READ_KEY=your-read-key
COSMIC_WRITE_KEY=your-write-key

We will use these with our dotenv package to allow our src files to access these variables when necessary.

Open up gatsby-node.js and add config variables to pages

We are now going to use Gatsby’s built-in node API to give each page in our site access to the environment variable we just created.  First, we will import the variables from our .env file using dotenv, then we will explicitly set each variable in our page’s context. Your file should look like this:

require("dotenv").config();
/**
 * Implement Gatsby's Node APIs in this file.
 *
 * See: https://www.gatsbyjs.org/docs/node-apis/
 */

exports.onCreatePage = ({ page, actions }) => {
  const { createPage, deletePage } = actions

  deletePage(page)
  createPage({
    ...page,
    context: {
      writeKey: `${process.env.COSMIC_WRITE_KEY}`,
      readKey: `${process.env.COSMIC_READ_KEY}`,
      cosmicBucket: `${process.env.COSMIC_BUCKET}`
    }
  })
}

Creating our first page

Now we are going to create our first page that will grab all of the documentation objects and display them at the root of our site,  on index.js.  First, let’s create our list by creating a folder in the components directory titled docs – /src/components/docs/ and in that folder, we will create a file titled index.js.

This will be the module that is first displayed when we render our page after fetching our docs.  Here is the source code:

import React from 'react'import React from 'react'
import PropTypes from 'prop-types'
import { Link } from 'gatsby'

// formats our slugs to be used in links to our display page
function formatSlug(title) {
  return title
    .toLowerCase()
    .replace(/[^a-zA-Z ]/g, "")
    .replace(/\s/g, '-')
}

// formats datestrings into an object so that they can
// easily be used for display
function formatDate(dateString) {
  const date = new Date(dateString)
  const months = [
    'January',
    'February',
    'March',
    'April',
    'May',
    'June',
    'July',
    'August',
    'September',
    'October',
    'November',
    'December',
  ]
  const hh = date.getHours()
  let minutes = date.getMinutes()
  let hour = hh
  let dayTime = 'AM'
  if (hour >= 12) {
    hour = hh - 12
    dayTime = 'PM'
  }
  if (hour == 0) {
    hour = 12
  }

  minutes = minutes < 10 ? '0' + minutes : minutes

  return {
    year: date.getFullYear(),
    month: months[date.getMonth()],
    date: date.getDate(),
    hour: hour,
    minutes: minutes,
    dayTime: dayTime,
  }
}

const Docs = ({ docs }) => (
  <div className="docs-container">
    <div className="docs-list">
      {docs.map(doc => {
        const date_created = formatDate(doc.created_at)
        return (
          <Link
            key={doc._id}
            to={`/doc/${formatSlug(doc.title)}`}
            className="docs-item"
          >
            <div className="date-container">
              <h3 className="date-yearMonthDate">{`${date_created.month} ${
                date_created.date
                }, ${date_created.year}`}</h3>
              <p className="date-timeDayTime">{`at ${date_created.hour}:${
                date_created.minutes
                } ${date_created.dayTime}`}</p>
            </div>
            <h2 className="doc-title">{doc.title}</h2>
            <div
              className="doc-preview"
              dangerouslySetInnerHTML={{
                __html: doc.content,
              }}
            />
          </Link>
        )
      })}
    </div>
  </div>
)

Docs.propTypes = {
  docs: PropTypes.array.isRequired,
  pageContext: PropTypes.object,
}

export default Docs

What’s going on here:

This page basically runs a big loop over our docs  and returns some fancy jsx.  We map through the docs array and produce a Link from Gatsby that will contain the title, a date, and some content that uses a description for the piece of documentation that was published.

Feel free to add any .scss files to this directory as well to get a styling that works for you for the given class names.

Update the ‘home’ page with our new components

Now we can open up our home page file at /pages/index.js and import the components we just created and add them to our returned jsx.

import React from 'react'
import PropTypes from 'prop-types'
import { graphql } from 'gatsby'

import Layout from '../components/layout'
import SEO from '../components/seo'
import Docs from '../components/docs'

const IndexPage = ({ data, pageContext }) => {
  return (
    <Layout>
      <SEO
        title="Home"
        keywords={[
          `gatsby`,
          `application`,
          `react`,
          'documentation',
          'docs',
          'markdown',
        ]}
      />
      <Docs docs={data.docs.objectsByType} pageContext={pageContext} />
    </Layout>
  )
}

IndexPage.propTypes = {
  data: PropTypes.object,
  pageContext: PropTypes.object,
}

export const query = graphql`
  query($cosmicBucket: String!, $readKey: String!) {
    docs {
      objectsByType(
        bucket_slug: $cosmicBucket
        type_slug: "docs"
        read_key: $readKey
      ) {
        title
        content
        created_at
        _id
      }
    }
  }
`

export default IndexPage

Now any docs created on Cosmic JS, will appear here on the home page!

Notice the exported query at the bottom of the file.  It contains two string-type variables that will be present because we set the context object in our gatsby-node configuration.

With our newly created home page working, let’s create our doc view that will display content from the documentation that we post.

Creating our doc display page

Instead of adding a new file to the pages directory in Gatsby, we are going to create a templates directory and make a template page that we can configure on the build, so that each time a doc is created, a new page can be created when we fetch our Docs from Cosmic JS.

Start by creating a templates directory at your project root, and then create a docPage.js file within `templates`

Now add the page template complete with an exported query that will fetch a singular doc from Cosmic JS:

import React from "react";
import showdown from "showdown";
import PropTypes from "prop-types";
import Layout from "../components/layout.js";
import SEO from "../components/seo.js";
import { graphql, Link } from "gatsby";
import hljs from "highlight.js";
import "highlight.js/styles/github.css";

const converter = new showdown.Converter({ ghCompatibleHeaderId: true });

class DocPage extends React.PureComponent {
  constructor(props) {
    super(props);
    this.state = {
      Doc: props.data.docs.object,
    };
  }

  componentDidMount() {
    hljs.initHighlighting();
  }

  componentWillUnmount() {
    hljs.initHighlighting.called = false;
  }

  render() {
    let toc;
    let doc;
    for (const i in this.state.Doc.metafields) {
      if (this.state.Doc.metafields[i].key === "table_of_contents") {
        toc = this.state.Doc.metafields[i].value;
      }
      if (this.state.Doc.metafields[i].key === "documentation") {
        doc = this.state.Doc.metafields[i].value;
      }
    }
    return (
      <Layout selectedDoc={this.state.Doc}>
        <SEO
          title={this.state.Doc.title}
          keywords={[`${this.state.Doctitle}`, "gatsby", "documentation"]}
        />
        <div className="doc-container">
          <div className="toc-container">
            <div className="back-bttn">
              <i className="arrow left" />
              <Link to="/">Back To List</Link>
            </div>
            <div
              className="doc-toc"
              dangerouslySetInnerHTML={{ __html: converter.makeHtml(toc) }}
            />
          </div>
          <div
            className="doc-main"
            dangerouslySetInnerHTML={{ __html: converter.makeHtml(doc) }}
          />
        </div>
      </Layout>
    );
  }
}

export const query = graphql`
  query($cosmicBucket: String!, $title: String!, $readKey: String!) {
    docs {
      object(bucket_slug: $cosmicBucket, slug: $title, read_key: $readKey) {
        title
        content
        created_at
        _id
        metafields {
          key
          value
        }
      }
    }
  }
`;

DocPage.propTypes = {
  data: PropTypes.object.isRequired,
};

export default DocPage;

Nothing will happen with this template until we tell Gatsby that it needs to create a page using this template. We do this so that Gatsby has a chance to fetch our Documentation from Cosmic JS before it builds the page using the necessary parameters for each GraphQL query at the bottom of docPage.js.  We are using static site files after all.

Update Gatsby Node to build template pages

Let’s go ahead and add an export function to gatsby-node.js so that we are building a doc Page template from our GraphQL data:

require("dotenv").config();
const path = require('path');
/**
 * Implement Gatsby's Node APIs in this file.
 *
 * See: https://www.gatsbyjs.org/docs/node-apis/
 */

exports.onCreatePage = ({ page, actions }) => {
  const { createPage, deletePage } = actions

  deletePage(page)
  createPage({
    ...page,
    context: {
      writeKey: `${process.env.COSMIC_WRITE_KEY}`,
      readKey: `${process.env.COSMIC_READ_KEY}`,
      cosmicBucket: `${process.env.COSMIC_BUCKET}`
    }
  })
}

// Gatsby's built in createPages API lets you
//  explicitly create a page route from a template
exports.createPages = ({ graphql, actions }) => {
  const { createPage, createRedirect } = actions

  // configures our route and redirect slug
  createRedirect({
    fromPath: `/doc/*`,
    isPermanent: true,
    redirectInBrowser: true,
    toPath: `/`,
  })

  // This promise makes a graphql request and builds
  //  a page using the returned data and our docTemplate
  return new Promise((resolve, reject) => {
    const docTemplate = path.resolve(`src/templates/docPage.js`)

    resolve(
      graphql(`
        query {
          docs {
            objectsByType(bucket_slug: "${process.env.COSMIC_BUCKET}", type_slug: "docs", read_key: "${process.env.COSMIC_READ_KEY}") {
              title
            }
          }
        }
      `
      ).then(result => {
        if (result.errors) {
          reject(result.errors)
        }
        result.data.docs.objectsByType.forEach(doc => {
          let slug = doc.title.toLowerCase().replace(/\s/g, '-')
          createPage({
            path: `/doc/${slug}`,
            component: docTemplate,
            context: {
              cosmicBucket: `${process.env.COSMIC_BUCKET}`,
              readKey: `${process.env.COSMIC_READ_KEY}`,
              title: slug,
            }
          })
        })
      })
    )
  })
}

Now when Gatsby creates its pages, ie – the index page will fetch our docs. Create a page for each doc that is retrieved, and attach all the necessary parameters to the page.  So our template component is rendered and our GraphQL query will succeed!

3.0 Deployment

Lastly, we can talk about deployment and about how static sites work.  Deploying this bad boy can be a little tricky as this site uses a static build. It won’t have the necessary pages of newly created docs until the deployment service has a chance to rebuild.

My recommendation is to use netlify and link your source from GitHub or wherever you store your code.  From there you can trigger build hooks in order to rebuild your site whenever certain events happen.  Cosmic JS allows a post request to be fired off at the endpoint when objects are created, deleted, edited, etc.  So you can easily link the two together to make some magic happen.  Keep in mind, if you want to allow users to create Docs from within your UI. We will fire a POST request manually to activate a build hook when a doc is successfully created.

Anyway, that’s all for me folks! Happy hacking.