Monday, 29 April, 2019 UTC


Summary

This is the 2nd part of a tutorial series on building fast and elegant sites with Gatsby, Material-ui, and NetlifyCMS (part 1 here). The previous part of the tutorial showed how to setup Gatsby and create pages manually as well as dynamically using gatsby-node.js. This part of the series will show the following:
  • Creating the Courses Page and Individual Courses
  • Adding Images to the Courses/Posts
  • Install Material-UI plugins and style all pages with Material-UI
  • Installing NetlifyCMS to allow non-technical users to make content changes
  • Adding More Fields Types to the Courses (that work out of the box with NetlifyCMS)
Demo Site (Here’s where we’re going to get to)
Github Repo (If you’d like to jump right into the code)

Creating the Courses Page and Individual Courses

Our demo site is appendTo, which features both blogs (which we did) and courses (which we’ll do now). Similar to the blogs, this involves creating a parent /courses page, a course template, a courseitem component (for the list on /courses, and the markdown files for the courses. So let’s first create the main /courses page (that will list all of the courses) and pages for each course.
Here’s how I created each of the pages:
/pages/courses.js
import React from "react"
import { Link, StaticQuery, graphql } from "gatsby"

import Layout from "../components/layout"
import SEO from "../components/seo"
import CourseItem from "../components/CourseItem"

class CoursePage extends React.Component {
 render() {
   const { data } = this.props

   const { edges: posts } = data.allMarkdownRemark

   return (
     <div>
       <Layout>
         <SEO title="Courses Page" />
         <h1>Courses Page</h1>

         <div
           style={{
             display: "flex",
             justifyContent: "space-evenly",
             flexWrap: "wrap",
           }}
         >
           {posts &&
             posts.map(({ node: post }) => (
               <CourseItem
                 key={post.id}
                 post={post.frontmatter}
                 style={{ marginRight: 10, width: "50%" }}
                 slug={post.fields.slug}
                 excerpt={post.excerpt}
               />
             ))}
         </div>
       </Layout>
     </div>
   )
 }
}

export default () => (
 <StaticQuery
   query={graphql`
     query CoursePageQuery {
       allMarkdownRemark(
         sort: { order: DESC, fields: [frontmatter___date] }
         filter: { frontmatter: { templateKey: { eq: "single-course" } } }
       ) {
         edges {
           node {
             excerpt(pruneLength: 100)
             id
             fields {
               slug
             }
             frontmatter {
               title
               templateKey
               date(formatString: "MMMM DD, YYYY")
             }
           }
         }
       }
     }
   `}
   render={data => <CoursePage data={data} />}
 />
)
/templates/single-course.js
import React from "react"
import { graphql } from "gatsby"
import Layout from "../components/layout"
import { Link } from "gatsby"

const CoursePage = ({ data }) => {
 const { markdownRemark: post } = data

 return (
   <Layout>
     <Link to="/courses">
       <p>← Back to Courses</p>
     </Link>

     <h1>{post.frontmatter.title}</h1>
     <h4>{post.frontmatter.date}</h4>
     <p dangerouslySetInnerHTML={{ __html: post.html }} />
   </Layout>
 )
}

export default CoursePage

export const CoursePageQuery = graphql`
 query CoursePage($id: String!) {
   markdownRemark(id: { eq: $id }) {
     html
     frontmatter {
       title
     }
   }
 }
`
/components/courseitem.js
import React from "react"
import { Link } from "gatsby"

function CourseItem(props) {
 const { post, slug, excerpt } = props

 return (
   <div>
     <Link to={slug}>
       <h1>{post.title}</h1>
     </Link>
     <h3>{excerpt}</h3>
   </div>
 )
}

export default CourseItem
/courses/intermediate-react.md
---

templateKey: single-course

title: intermediate React

date: 2019-04-15T16:43:29.834Z

description: An intermediate React course

difficulty: Intermediate

---

what a course, what a course…….
/courses/cool-course.md etc
---

templateKey: single-course

title: Great course on JS

date: 2019-04-15T16:43:29.834Z

description: A great course

difficulty: Beginner

---

what a course, what a course...what a course, what a coursewhat a course, what a course...what a course, what a course

what a course, what a course...what a course, what a course

what a course, what a course...what a course, what a course

what a course, what a course...what a course, what a course
What’s going on here? Let’s review:
Gatsby-node.js creates the course pages using the appropriate template with this section of the code:
...

component: path.resolve(

`src/templates/${String(edge.node.frontmatter.templateKey)}.js`

),

…
In this case, it would be using single-course, of course.
Within single-course.js there is a page query at the bottom. This queries for the right markdown file (using the id found in context) so that those specific pages will have the right title and html to display.
Within the courses.js file, we’re using a Static Query to query/search for the courses that have the templateKey of single-course and making a list of the courses (using the simple CourseItem.js component for help). This gives us a very basic (but functional) /courses page like this:
And an individual course page like this:
So these are pretty bland pages, unless you’re a hardcore minimalist. I’ll show you some Gatsby image plugins now that will add some color/life to these posts/courses.

Adding Images to the Courses/Posts

One of the top selling points of Gatsby is speed. Poorly sized or slow loading images are one of the easiest ways to make a website feel sluggish so this is an obvious area to optimize first. Two Gatsby plugins (gatsby-image and gatsby-transformer-sharp) are amazing tools for working with and optimizing Gatsby images. The latter leverages the Sharp image library, which is a very popular node module for resizing images. I’ll show you now to how add images to the course and post pages.
First, we need to install the plugins:
npm install --save gatsby-image gatsby-transformer-sharp gatsby-plugin-sharp
We’re also going to add a plugin that will allow us to access relative image paths in the project:
npm install gatsby-remark-relative-images --save
This will make for working with netlifyCMS images easier later.
Then add them to the gatsby-config.js, within the big array of plugins like so:
'gatsby-plugin-sharp',
    'gatsby-transformer-sharp',
    {
      resolve: 'gatsby-transformer-remark',
      options: {
        plugins: [
          {
            resolve: 'gatsby-remark-relative-images',
          },
          {
            resolve: 'gatsby-remark-images',
            options: {
              // It's important to specify the maxWidth (in pixels) of
              // the content container as this plugin uses this as the
              // base for generating different widths of each image.
            
              maxWidth: 590,
            },
          },
          {
            resolve: 'gatsby-remark-copy-linked-files',
            options: {
              destinationDir: 'static',
            },
          },
        ],
      },
    },
{
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/static/img`,
        name: 'uploads',
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/pages`,
        name: 'pages',
      },
    },
    {
      resolve: 'gatsby-source-filesystem',
      options: {
        path: `${__dirname}/src/images`,
        name: 'images',
      },
    },
Now we can add the images to our posts. Create a folder called /static and a folder within that call img. That’s where the images will live. I downloaded some demo images for this fake courses and put them in the folder.
Now within the individual course markdown files, we need to add the images within the frontmatter (—) headings like so:
---

[more items]

image: /img/[imagename].jpeg

---
Now, we’ll edit the single-course.js page to be able to query for this image and render it on the page. Change the GraphQL query at the bottom of the page to include the image query, like so:
export const CoursePageQuery = graphql`
  query CoursePage($id: String!) {
    markdownRemark(id: { eq: $id }) {
      html
      frontmatter {
        title
        image {
          childImageSharp {
            fluid(maxWidth: 500, quality: 100) {
              ...GatsbyImageSharpFluid
            }
          }
        }
      }
    }
  }
`
This is using the gatsby-plugin-sharp plugin to render a fluid image with a maxWidth of 500 pixels. Right here within the GraphQL query, we’re able to choose how we’d like to receive the image in our component/html. This plugin has extensive documentation that can be found here.
Now, within the the single-course.js, we’ll use Gatsby-image to render the image in the html. It’s a simple import:
import Img from "gatsby-image"
And then a simple enough component to render.
<Img fixed={post.frontmatter.image.childImageSharp.fluid} />
This is how you add the right plugins, the files, and the queries to be able to use plugins within Gatsby.

Installing NetlifyCMS

NetlifyCMS (like an CMS) allows for non-technical users to edit content on a site. This allows people in marketing or content roles the freedom to create and frees up developers from having to do these sorts of small tasks. As was mentioned before, NetlifyCMS is made by static hosting service Netlify, but they can each be used separately from each other. Gatsby can be easily configured to work with WordPress, Contently, and many other data sources. NetlifyCMS has great docs, a generous free tier, and easy authentication.
NetlifyCMS is easy to configure with Gatsby using, you probably guessed it, gatsby-plugin-netlify-cms. Install it using npm and add it to gatsby-config.js:
npm install gatsby-plugin-netlify-cms --save
// gatsby-node.js
},.......[other plugins]

`gatsby-plugin-netlify-cms`,

….[other plugins] {
NetlifyCMS needs a config.yml file, which generally tells it which fields can be changed within the CMS. Create a folder called ‘admin’ within the Static folder and put the following config.yml file in it:
backend:
  name: git-gateway
  branch: master

media_folder: static/img
public_folder: /images

collections:
  - name: "pages"
    label: "Pages"
    files:    
      - file: "src/pages/about.md"
        label: "About"
        name: "about"
        fields:
          - {
              label: "Template Key",
              name: "templateKey",
              widget: "hidden",
              default: "about-page",
            }
          - { label: "Title", name: "title", widget: "string" }
          - { label: "Body", name: "body", widget: "markdown" }
  - name: "blog"
    label: "Blog"
    folder: "src/pages/blogs"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {
          label: "Template Key",
          name: "templateKey",
          widget: "hidden",
          default: "single-blog",
        }
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Publish Date", name: "date", widget: "datetime" }
      - { label: "Description", name: "description", widget: "text" }
      - { label: "Body", name: "body", widget: "markdown" }

  - name: "courses"
    label: "Courses"
    folder: "src/pages/courses"
    create: true
    slug: "{{year}}-{{month}}-{{day}}-{{slug}}"
    fields:
      - {
          label: "Template Key",
          name: "templateKey",
          widget: "hidden",
          default: "single-course",
        }
      - { label: "Title", name: "title", widget: "string" }
      - { label: "Publish Date", name: "date", widget: "datetime" }
      - { label: "Description", name: "description", widget: "text" }
      - { label: "Body", name: "body", widget: "markdown" }
      - { label: Image, name: image, widget: image }
This creates 3 collections within the CMS: one for updating the about page, one for the courses, and one for the blogs. The ‘widgets’ are NetlifyCMS widgets and should be self-explanatory in a moment. With the above config.yml, this is what my admin interface looks like:
Within the Courses Collection, I see this:
Take a look, again, at the config.yml.
Notice how the fields with this file line up with what you’re seeing in the CMS image above? The template key is hidden so it doesn’t show. But right there are string widget (aka text field) for Title, Description, and Body. And there’s a datetime widget (a datepicker) for Publish Data. NetlifyCMS is really just an interface for making changes to our markdown files here. The config.yml serves as a way to tell Netlify which fields are available for editing. NetlifyCMS features a wide variety of default widgets and you can even make custom ones.
Source: https://www.netlifycms.org/docs/widgets/
Although this takes some setup work, this workflow is extremely powerful for using Github markdown files as a CMS.
The final thing needed here is making the images hosted in static/img available for NetlifyCMS to use.
Add this to the top of the file within Gatsby-node.js
const { fmImagesToRelative } = require('gatsby-remark-relative-images')
And add this method call within the onCreateNode block:
exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions
fmImagesToRelative(node)
This plugin was specifically built to make NetlifyCMS play nice with relative image paths. Failure to add this config right will send you to bug hell.

Styling it all with Material-UI

We’ve built out our posts, blogs, and about page. Now we’ll see how to style out all of the pages using the most popular React UI framework, Material-ui. Material-ui has nearly 50,000 stars on Github and over 1,000 contributors. It is a React implementation of Google’s Material Design principles. Material-ui gives React developers a wide variety of components, styling, and utilities for making their sites aesthetically pleasing and easy to use. If you don’t want your site to look like a generic Googly app, Material-ui is supremely easy to customize to your liking.
There are many ways to add Material-ui to a Gatsby project. Material-ui has a Gatsby starter to use. Another developer made a dedicated Gatsby-starter with Material-ui. But I find the easiest way to add Material-ui to Gatsby is with gatsby-plugin-material-ui. The previous two totally work but they are much more complicated. This is the beauty of Gatsby: Just install the plugin, add it to gatsby-config.js and you’re good to go. Here’s how to do just that:
npm install --save gatsby-plugin-material-ui@next
Edit gatsby-config.js
module.exports = {
  plugins: [
    {
      resolve: `gatsby-plugin-material-ui`,
      options: {
      },
    },
  ],
};
If you’ve worked with Material-ui before, you’ve probably played around with its theme. Material-ui themes allow you to establish a global theme that can be called from various child components. This makes it easier to create one coherent look and feel (vs. having pages/components with different styles, which can look sloppy). With the gatsby-plugin-material-ui plugin, this theme will go in that options object in the config above like so:
options: {        
        theme: {
          palette: {
              primary: {
                  main: '#BA3D3B', // new color here 
              } 
          },
      },
Now we can import material-ui components anywhere within the our Gatsby project. Here’s how I changed the single-course.js page:
imports...
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
...
import Avatar from '@material-ui/core/Avatar';

const styles = theme => ({
  root: {
    flexGrow: 1,
  },
  paper: {
    padding: theme.spacing.unit * 2,
    textAlign: 'center',
    color: theme.palette.text.secondary,
  },
  title:{
    marginBottom:'.2em'
  },
  backButton: {
    textDecoration:'none'
  },
  bigAvatar: {
    '& img':{
      margin: 10,
      width: 60,
      height: 60,
    },
    width:100,
    height:100,
    border:'1px solid grey'
  
  },
});

const CoursePage = ({ data, classes }) => {
  const { markdownRemark: post } = data

  return (
    <Layout>
       <Link to='/courses' className={classes.backButton}>
      <p>← Back to Courses</p>

      </Link>
      
      <Grid container spacing={24}>
       
        <Grid item xs={9}>
          <h1 className={classes.title}>{post.frontmatter.title}</h1>
          <h4>{post.frontmatter.date}</h4>
          <p dangerouslySetInnerHTML={{ __html: post.html }}/>  
        </Grid>    
        <Grid item xs={3}>
        <Avatar src={post.frontmatter.image.childImageSharp.fluid.src} className={classes.bigAvatar} />
       
        </Grid>   
      </Grid>
  
    </Layout>
  )
}
export default withStyles(styles)(CoursePage);

[page query removed for brevity]
Here’s how I changed the single-blog template:
...
import { withStyles } from '@material-ui/core/styles';
import Grid from '@material-ui/core/Grid';
...
import Layout from '../components/layout'
import Img from "gatsby-image"
import { Link } from "gatsby"

const styles = theme => ({
  root: {
    flexGrow: 1,
  },
  paper: {
    padding: theme.spacing.unit * 2,
    textAlign: 'center',
    color: theme.palette.text.secondary,
  },
  title:{
    marginBottom:'.2em'
  },
  backButton: {
    textDecoration:'none'
  }
});

const BlogPage = ({ data, classes }) => {
  const { markdownRemark: post } = data

  return (
    <Layout>
      <div className={classes.root}>
      <Link to='/blog' className={classes.backButton}>
      <p>← Back to Blog</p>

      </Link>
      
      <Grid container spacing={24}>
        <Grid item xs={3}>
          <Img fluid={post.frontmatter.image.childImageSharp.fluid} />
        </Grid>
        <Grid item xs={9}>
          <h1 className={classes.title}>{post.frontmatter.title}</h1>
          <h4>{post.frontmatter.date}</h4>
          <p dangerouslySetInnerHTML={{ __html: post.html }}/>  
        </Grid>       
      </Grid>
      </div>
    
    </Layout>
  )
}


export default withStyles(styles)(BlogPage);

[page query removed for brevity]
`
Here’s the about page:
...
import { withStyles } from '@material-ui/core/styles';

const styles = theme => ({
  heroText: {
    color:'white',
    textAlign: 'center',
    lineHeight:7,
    marginTop:-20
  },
  mainBlogCopy: {
    marginTop: theme.spacing.unit,
  },
  blogText:{
    color:theme.palette.primary.main
  }
});

const AboutPage = ({ data, classes }) => {
  const { markdownRemark: post } = data

  return (
    <Layout>
      <div style={{
        backgroundImage: `url(${post.frontmatter.image.childImageSharp.fluid.src})`,
        height:300
      }}>
      <h1 className={classes.heroText}>{post.frontmatter.title}</h1>
      </div>
      <div className={classes.mainBlogCopy}>
       <p className={classes.blogText} dangerouslySetInnerHTML={{ __html: post.html }}/> 
      </div>
      
    </Layout>
  )
}

AboutPage.propTypes = {
  data: PropTypes.object.isRequired,
}

export default withStyles(styles)(AboutPage);

[page query removed for brevity]
Here’s what I added to the home page to make it look a little more like appendTo:
….
import { Link, StaticQuery, graphql } from "gatsby"
import Grid from "@material-ui/core/Grid"
import BlogItem from "../components/BlogItem"
import { withStyles, withTheme } from "@material-ui/core/styles"
import Button from "@material-ui/core/Button"
import Typography from '@material-ui/core/Typography'
import AppBar from '@material-ui/core/AppBar';
import Tabs from '@material-ui/core/Tabs';
import Tab from '@material-ui/core/Tab';


const styles = theme => ({
  mainBlogArea: {
    paddingTop: '20px !important',

  },
  redBox:{
    padding:30,
    paddingTop:50,
    height:200,
    backgroundColor:'#AC4839',
    marginBottom:30
  },
  greyBox:{
    padding:30,
    paddingTop:50,
    height:200,
    backgroundColor:'#D9D8D8'
  },
  blackButton:{
    backgroundColor:'black',
    color:'white'

  },
  redButton:{
    backgroundColor:'#AC4839',
    color:'white'

  },
  TabsSection:{
    marginTop:30,
    backgroundColor:'white',
    border:'1px solid grey',
    height:300,
  },
  Tab:{
      width:10
  }

  
})

const IndexPage = props => {
  const { data, classes } = props
  // const { edges: posts } = data.allMarkdownRemark

  return (
    <Layout>
      <SEO title="appendTo Home" keywords={[`Courses`, `Training`, `react`]} />

      <Grid container spacing={24}  className={classes.mainBlogArea}>
        <Grid item xs={8}>
          <div >
            {data.map(item => (
              <BlogItem
                key={item.id}
                post={item.frontmatter}
                image={item.frontmatter.image.childImageSharp.fluid.src}
                slug={item.fields.slug}
                date={item.frontmatter.date}
              />
            ))}
          </div>
        </Grid>
        <Grid item xs={4}>
          <div className={classes.redBox}>
            <Typography variant="h5" style={{color:'white'}}>
              Custom Private Courses
            </Typography>
            <Button variant="contained" className={classes.blackButton}>
              Get Started
            </Button>
          </div>

          <div className={classes.greyBox}>
          <Typography variant="h5">
              Live Public Courses
            </Typography>
            <Button variant="contained" className={classes.redButton}>
              Sign Up Today
            </Button>
          </div>

          <div className={classes.TabsSection} >
          <AppBar position="static">
            <Tabs>
              <Tab label="Popular" className={classes.Tab} />
              <Tab label="Recent" className={classes.Tab} />
        
            </Tabs>
            </AppBar>


          </div>
        </Grid>
      </Grid>
    </Layout>
  )
}

const StyledUpIndexPage = withStyles(styles)(IndexPage)

export default () => (
  <StaticQuery
    query={graphql`
      query IndexPageQuery {
        allMarkdownRemark(
          sort: { order: DESC, fields: [frontmatter___date] }
          filter: { frontmatter: { templateKey: { eq: "single-blog" } } }
        ) {
          edges {
            node {
              excerpt(pruneLength: 40)
              id
              fields {
                slug
              }
              frontmatter {
                title
                templateKey
                date(formatString: "MMMM DD, YYYY")
                image {
                  childImageSharp {
                    fluid(maxWidth: 1400, quality: 100) {
                      ...GatsbyImageSharpFluid
                    }
                  }
                }
              }
            }
          }
        }
      }
    `}
    render={data => (
      <StyledUpIndexPage
        data={data.allMarkdownRemark.edges.map(item => item.node)}
      />
    )}
  />
)
And finally, here’s what I added to the NavBar to allow for navigating between the pages:
import { Link } from "gatsby"
import React from "react"
import { withStyles } from "@material-ui/core/styles"
import AppBar from "@material-ui/core/AppBar"
import Toolbar from "@material-ui/core/Toolbar"
import Button from "@material-ui/core/Button"

import { StaticQuery, graphql } from "gatsby"
import Img from "gatsby-image"


const styles = {
  root: {
    flexGrow: "1 !important",
  },
  appbar: {
    backgroundColor: "#AC493A",
  },
  grow:{
    flexGrow:1
  },
  link: {
    color: `white`,
    textDecoration: `none`,
  },
}

class Header extends React.Component {
  constructor(props) {
    super(props)
  }

  render() {
    const { classes } = this.props

    return (
      <div className={classes.root}>
        <AppBar position="static" className={classes.appbar}>
          <Toolbar>
            <div className={classes.grow}>
              <Link to='/' >
              <StaticQuery
                query={graphql`
                  query {
                    file(relativePath: { eq: "appendto_logo.png" }) {
                      childImageSharp {
                        # Specify the image processing specifications right in the query.
                        # Makes it trivial to update as your page's design changes.
                        fixed(width: 150) {
                          ...GatsbyImageSharpFixed_noBase64
                        }
                      }
                    }
                  }
                `}
                render={data => <Img  critical={true} fadeIn fixed={data.file.childImageSharp.fixed} />}
              />
              </Link>
            </div>
            <div>
              <Link to="/about" className={classes.link}>
                <Button color="inherit">About</Button>
              </Link>

              <Link to="/blog" className={classes.link}>
                <Button color="inherit">Blog</Button>
              </Link>

              <Link to="/courses" className={classes.link}>
                <Button color="inherit">Courses</Button>
              </Link>
            </div>
          </Toolbar>
        </AppBar>
      </div>
    )
  }
}

export default withStyles(styles)(Header)
And here’s what we end up with: https://appendtostaticstyled.netlify.com/
We have here a site that we can continue to build upon and style with Material-ui. All users can make tweaks or changes using NetlifyCMS. And finally, this site is blazing fast and consists of static files that can be served for cloud storage service like S3 and/or a CDN.
This starter project is a great way to start any project and I hope these two long posts helped you understand how it came together.
The post Gatsby Tutorial Post Part 2: NetlifyCMS and Styling appeared first on appendTo.