DEV Community

Cover image for HowTo Gatsby + NetlifyCMS static and dynamic pages
alfchee
alfchee

Posted on • Edited on

HowTo Gatsby + NetlifyCMS static and dynamic pages

Our Website and some other websites we've built in JAMstack has been with the stack of: GitLab, Netlify, Gatsby and Netlify CMS. This allow us to build a website with React Components with content sourced in markdown files, managed by Netlify CMS and autodeployed using the Netlify service.

Our JAMstack

JAMstack stands for JavaScript, APIs, and Markup. The term was coined by Mathias Biilmann to describe a modern web development architecture based on client-side JavaScript, reusable APIs, and prebuilt Markup.

Netlify CMS

Netlify CMS is an open source content management system for your Git workflow that enables you to provide editors with a friendly UI and intuitive workflows. You can use it with any static site generator to create faster, more flexible web projects. Content is stored in your Git repository alongside your code for easier versioning, multi-channel publishing, and the option to handle content updates directly in Git.

You may read more in the official documentation.

Gatsby

Gatsby is a free and open source framework based on React that helps developers build blazing fast websites and apps. Forget complicated deploys with databases and servers and their expensive, time-consuming setup costs, maintenance, and scaling fears. Gatsby.js builds your site as “static” files which can be deployed easily on dozens of services.

You may read more in the official documentation.

Configuring Gatsby with Netlify CMS

Add the dependencies

First we need install some dependencies to make them work together

npm install --save netlify-cms-app gatsby-plugin-netlify-cms netlify-cms-app
Enter fullscreen mode Exit fullscreen mode

Configuration for Netlify CMS

We need to create a file that tells to Netlify CMS which is our structure of data and where is stored, along with some other configuration. The file must be at static/admin/config.yml

├── static
│   ├── admin
│   │   ├── config.yml
Enter fullscreen mode Exit fullscreen mode
backend:
  name: gitlab
  repo: alfchee/alfchee-site
  branch: master

media_folder: "static/assets"
public_folder: assets
slug:
  encoding: "ascii"
  clean_accents: true
  sanitize_replacement: "_"

collections:
  - name: 'pages'
    label: 'Pages'
    folder: 'content/pages'
    create: true
    editor:
      preview: false
    fields:
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Description", name: "description", widget: "string" }
      - {
          label: "Post Image",
          name: "featuredImage",
          widget: "image",
          allow_multiple: false,
        }
      - { label: "Template Key", name: "templateKey", widget: "string" }
      - { label: "Body", name: "body", widget: "markdown" }
Enter fullscreen mode Exit fullscreen mode

With the previous we are configuring our backend to be GitLab and setting the repository name and the branch where Netlify CMS will be pushing changes.

Next we use media_folder and public_folder to tell where our assets will be stored when we upload files with the CMS, this come handy because we may specify the same directory used by Gatsby.

Then the Slug section allow us to create rules about how to create the slugs for the pages.

Lastly, in the section of collections we may create as many collections we whish, and define the structure of data for them, in this way the CMS will show many fields as we specify. In this example we are creating a collection of pages, where they'll be stored as markdown files in the directory content/pages, creating one file per page created. Then forl all the pages, the list of fields is declared. You may read more about the definition of collections and the available widgets to set the fields.

Accessing the content from Gatsby

Now that you have set your data and schema, you need to fetch it so can be rendereable, for so we need to make some configurations in Gatsby.

Dependencies

Make sure you have the next dependencies installed, they are Gatsby Plugins

npm install --save gatsby-source-filesystem gatsby-transformer-remark gatsby-remark-images gatsby-remark-responsive-iframe gatsby-remark-prismjs gatsby-remark-copy-linked-files gatsby-remark-smartypants gatsby-transformer-sharp gatsby-plugin-sharp prismjs gatsby-image 
Enter fullscreen mode Exit fullscreen mode

Configuring Gatsby

The Configuration of Gatsby is in the file gatsby-config.js, and in that file we'll configure the source of data and also the plugins we've installed.

module.exports = {
  /* Your site config here */
  plugins: [
    /* Pages */
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `${__dirname}/content/pages`,
        name: `pages`,
      },
    },
    {
      resolve: `gatsby-transformer-remark`,
      options: {
        plugins: [
          {
            resolve: `gatsby-remark-images`,
            options: {
              maxWidth: 590,
            },
          },
          {
            resolve: `gatsby-remark-responsive-iframe`,
            options: {
              wrapperStyle: `margin-bottom: 1.0725rem`,
            },
          },
          `gatsby-remark-prismjs`,
          `gatsby-remark-copy-linked-files`,
          `gatsby-remark-smartypants`,
        ],
      },
    },
    `gatsby-transformer-sharp`,
    `gatsby-plugin-sharp`,
    {
      resolve: `gatsby-plugin-netlify-cms`,
      options: {
        enableIdentityWidget: false,
      },
    },
    "gatsby-plugin-netlify",
  ]
}
Enter fullscreen mode Exit fullscreen mode

Each time you add a new directory with content that you need to be inspected by Gatsby then you'll need to add a block for gatsby-source-filesystem and setting the path. But the markdown files are just text files, in order they can be processed correctly you need the plugin gatsby-transformer-remark, that accepts some plugins to format the content, for so all the we've added in our configuration.

Then you can see that both plugins of Netlify has been added. The option enableIdentityWidget is required to set to false because we are using GitLab.

Static Pages in Gatsby

As you know, if you add files to src/pages directory, Gatsby will try to render them as pages and create routes. For so src/pages/index.jsx will be treated as the home page, and in the same way you can continue adding more static pages with static routes.

From a static page if you want to get information of your markdown files, you'll use GraphQL to query the data you need. For so you'll be using some objects provided by Gatsby. Example:

import { graphql } from "gatsby"
import React from "react"

class MyComponent extends React.Component {
  render() {
    // obtaining from props the data queried with GraphQL
    const page = this.props.data.markdownRemark
  }
}

export const query = graphql`
  query MyPage{
    markdownRemark({ WRITE YOUR QUERY }) {
      id
      html
      frontmatter {
        ANY OTHER FIELDS DEFINED
      }
    }
  }
`

export default IndexPage
Enter fullscreen mode Exit fullscreen mode

Dynamic Pages with Gatsby

Using the previous method we need to create new JS or JSX file for each page, and make the the same things once and another time. To avoid that we are going to dynamically create the pages, and to be able to do that we'll be using:

  • Layout, defining one or more layouts that will define shared behavior for all the site
  • Templates, defining shared behaviors or elements to be displayed in dependence of the page
  • Edit gatsby-node.js, in this file we can use some hooks that allow us to create the pages dynamically

Install dependencies

npm install esm
Enter fullscreen mode Exit fullscreen mode

Content for gatsby-node.js

If this file doesn't exist, create it or edit the content

require = require("esm")(module)
module.exports = require("./gatsby-node.esm.js")
Enter fullscreen mode Exit fullscreen mode

Now we need to create the file gatsby-node.esm.js and add the content

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

exports.onCreateNode = ({ node, actions, getNode }) => {
  const { createNodeField } = actions

  if (node.internal.type === `MarkdownRemark`) {
    const value = createFilePath({ node, getNode })
    createNodeField({
      name: `slug`,
      node,
      value,
    })
  }
}

const buildPages = (edges, createPage) => {
  const pages = edges.filter((edge) => {
    if ("templateKey" in edge.node.frontmatter) {
      return edge
    }
  })

  // create pages with the filtered edges
  pages.map((page) => {
    console.log(`Creating page ${JSON.stringify(page.node.frontmatter)}`)

    createPage({
      path: page.node.fields.slug,
      // getting the component to render the page using the templateKey property
      component: path.resolve(
        `src/templates/${String(page.node.frontmatter.templateKey)}.js`
      ),
      context: {
        id: page.node.id,
        slug: page.node.fields.slug,
      },
    })
  })
}

exports.createPages = async ({ graphql, actions }) => {
  const { createPage } = actions

  // graphql query for pages only
  const result = await graphql(
    `
      {
        allMarkdownRemark(
          limit: 1000
          filter: { fileAbsolutePath: { regex: "/(pages|blog)/i" } }
        ) {
          edges {
            node {
              id
              fields {
                slug
              }
              frontmatter {
                title
                templateKey
              }
            }
          }
        }
      }
    `
  )
  // if errors then throw
  if (result.errors) {
    throw result.errors
  }
  console.log("Creating pages ---->>>>")
  // Create pages
  buildPages(result.data.allMarkdownRemark.edges, createPage)
}
Enter fullscreen mode Exit fullscreen mode

The exposed function createPages will create dynaic pages according our needs. Using GraphQL we are querying files that has the word page or blog in the path (not file name), and we are interested in the values id, slug, title, and templateKey.

We defined the logic in the function buildPages to be able to handle more easy, we may create a function buildBlog to create only blog post pages with a different route. The interesting here is that createPage function needs the slug, which is the route for the URL and the component used to render this page, and is that the reason of the templateKey field, that we are using it to determine the file to use as template. So for a default templateKey, the file to use as template is src/templates/default.js.

The context is also important, so we make sure to pass the id and slug as context, because we can use them to query the content of the page we want to render.

Creating Templates

We may create many templates as we need to display or hide components or data in the dynamic pages, and we are going to define a default template. Create the file src/templates/default.js with the content

import React from "react"
import { graphql } from "gatsby"
import { Container } from "react-bootstrap"

import Layout from "../components/Layout"
import NavBar from "../components/NavBar"
import Footer from "../components/Footer"
import styles from "./default.module.css"

class PageDefaultTemplate extends React.Component {
  render() {
    const page = this.props.data.markdownRemark
    const meta = {
      pageTitle: page.frontmatter.title,
    }

    // TODO: Get the navbar items

    return (
      <Layout siteMeta={meta}>
        {/* Page content */}
        <section className={styles.sectionTerms}>
          {/* Navbar */}
          <NavBar />
          {/* end of Navbar */}

          <Container className={styles.contentData}>
            <header>
              <h1 className={styles.title}>{page.frontmatter.title}</h1>
            </header>

            <article
              className={styles.dataInfo}
              dangerouslySetInnerHTML={{ __html: page.html }}
            />
          </Container>

          {/* Footer */}
          <Footer />
        </section>
        {/* end of Page content */}
      </Layout>
    )
  }
}

export default PageDefaultTemplate

export const pageQuery = graphql`
  query PageBySlug($slug: String!) {
    markdownRemark(fields: { slug: { eq: $slug } }) {
      id
      html
      frontmatter {
        description
        featuredImage
        title
        templateKey
      }
    }
  }
`
Enter fullscreen mode Exit fullscreen mode

As seeing, the slug passed as context is used here to make a query in the markdownRemark collection to get the content of the page in which we are navigating, and then we can use it as we whish.

Layout component

Is good to have a layout component that handles SEO among other stuffs, but remember to have this kind of content in your layout

const Layout = ({ children, siteMeta }) => {
  const { pageTitle } = siteMeta

  return (
    <>
      <Seo pageTitle={pageTitle} />

      {/* main container for all the pages content */}
      <div className="main-container">{children}</div>
    </>
  )
}

Layout.propTypes = {
  children: PropTypes.node.isRequired,
  siteMeta: PropTypes.object,
}
Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
waynevanson profile image
Wayne Van Son

Thank you very much for writing this.

I haven't implemented my version of this, but it looks to me this is the article I'm looking for.