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 features both blogs (which we did previously) 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.js.
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 (from markdown) 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-to-use 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 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 (e.g. “Field “image” must not have a selection since type “String” has no subfields.”)
.
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 work but they are much more complicated. Plugins are one of the awesome parts 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 gatsby-plugin-material-ui @material-ui/styles
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 theming capabilities. 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 or 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.
Recommended Reading
This tutorial showed you the essentials of Gatsby. That said, even with nearly 5500 words, it had to skip over a number of topics. Here’s some posts I recommend reading to get a better understanding of Gatsby: