Angular Icon Get 73% off the Angular Master bundle

See the bundle then add to cart and your discount is applied.

0 days
00 hours
00 mins
00 secs

Write JavaScript like a pro. Javascript Icon

Follow the ultimate JavaScript roadmap.

Building a Blog With Gatsby and GraphQL

Building a custom blog doesn’t have to be hard, and you don’t have to do all of the hard work from scratch!

Gatsby, a static site generator for React, is an amazing tool that comes pre-configured with GraphQL and it allows you to easily get up and running.

Foundational Knowledge

Since Gatsby is a static site generator for React, you should have some React knowledge prior to taking this tutorial.

You should also be familiar with how to use the command line / terminal as we’ll be installing some packages with npm.

Setup

We need to install the Gatsby CLI (command-line-interface) to build new Gatsby projects.

Angular Directives In-Depth eBook Cover

Free eBook

Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.

  • Green Tick Icon Observables and Async Pipe
  • Green Tick Icon Identity Checking and Performance
  • Green Tick Icon Web Components <ng-template> syntax
  • Green Tick Icon <ng-container> and Observable Composition
  • Green Tick Icon Advanced Rendering Patterns
  • Green Tick Icon Setters and Getters for Styles and Class Bindings

To install the Gatsby CLI tool, run the following npm command:

npm install -g gatsby-cli

Building A Blog: The Short Way

Gatsby has a series of starter files called Gatsby Starters which allow you to quickly spin up different types of Gatsby sites.

Gatsby provides a starter blog which you can use to quickly get up-and-running.

If you aren’t familiar with Gatsby or GraphQL, I recommend following the in-depth tutorial to learn the in-and-outs.

To build a blog with the blog starter, simply run:

gatsby new <<my-blog-name>> https://github.com/gatsbyjs/gatsby-starter-blog

Then add your markdown blog files and customize to your heart’s content.

Building A Blog: The Long (But In-Depth) Way

Let’s walk through the steps of building a Gatsby blog from scratch using the default starter.

First, create your starter project by running:

gatsby new <<my-blog-name>> && cd <<my-blog-name>>
gatsby develop

When you open localhost:8000 in your browser, you will see the Gatsby default application.

Gatsby default starter

Let’s go ahead and remove all of the boilerplate. We will leave the current file structure inside of the src/ directory but remove all of the files inside.

rm -rf src/**/*.*

Gatsby Architecture

Since Gatsby is a static site generator for React, you can write simple React components, like you would do with create react app.

Here is the current architecture of our application:

Structure

Let’s go ahead and add some of the files that we’ll need to build our blog.

Our blog will have four pages:

Let’s create a JavaScript file for each of these pages inside of the pages directory:

Since we also removed all of the images from our project, we need to remove the reference to gatsby-icon to fix our development server.

Inside gatsby-config.js, remove the icon from the options object.

// delete me
icon: `src/images/gatsby-icon.png`

To check whether everything is working as expected, let’s have index.js in the pages/ directory return some simple JSX.

// pages/index.js
import React from "react";

const Home = () => (
  <div>
    <h1>Home</h1>
  </div>
);

export default Home;

When we restart our development server and head to our browser, we should see this:

Home page

Let’s add similar JSX into the other three page components:

// pages/about.js
import React from "react";

const About = () => (
  <div>
    <h1>About</h1>
  </div>
);

export default About;
// pages/blog.js
import React from "react";

const Blog = () => (
  <div>
    <h1>Blog</h1>
  </div>
);

export default Blog;
// pages/contact.js
import React from "react";

const Contact = () => (
  <div>
    <h1>Contact</h1>
  </div>
);

export default Contact;

If we head back to the browser and add a /about to the end of our localhost URL, we should see the about page. Likewise this will work for /blog and /contact.

So all of our pages are rendering, but wouldn’t it be nice if we had a navigation component we could use to switch between page views? Let’s build one!

First let’s create two new files in the components/ directory: Nav.js and nav.css.

Inside Nav.js add the following code:

// Nav.js
import React from "react";

const Nav = () => (
  <nav>
    <ul>
      <li>
        <a href="/">Home</a>
      </li>
      <li>
        <a href="/about">About</a>
      </li>
      <li>
        <a href="/blog">Blog</a>
      </li>
      <li>
        <a href="/contact">Contact</a>
      </li>
    </ul>
  </nav>
);

export default Nav;

Since we want the navigation bar on every single page, we could import it to each individual page and render it, however there’s an easier way.

We can use a <Layout> component to ensure our navigation is rendered on each page, without having to manually import and render it for each one.

This is what our <Layout> component will look like:

Layout

The navigation bar will sit at the top of the page, and all of the page content will be rendered in a <main> element beneath.

Inside components/ create Layout.js.

First, let’s import React, Prop Types, and our Nav component:

// Layout.js
import React from "react";
import PropTypes from "prop-types";

import Nav from "./Nav";

Next, we’ll create a stateless functional React component, passing children as a prop.

// Layout.js
const Layout = ({ children }) => (
  <>
    <Nav />
    <main>{ children }</main>
  </>
);

export default Layout;

We also want to ensure we’re passing JSX to the <Layout> component, so we’ll use PropTypes to enforce that.

// Layout.js
import React from "react";
import PropTypes from "prop-types";

import Nav from "./Nav";

const Layout = ({ children }) => (
  <>
    <Nav />
    <main>{ children }</main>
  </>
);

Layout.propTypes = {
  children: PropTypes.node.isRequired
};

export default Layout;

Let’s see if our <Layout> component works.

Back in index.js, let’s import our <Layout> component and render it around our page content.

// index.js
import React from "react";
import Layout from "../components/Layout";

const Home = () => (
  <Layout>
    <h1>Home</h1>
  </Layout>
);

export default Home;

If we head over to our browser we should see the navigation appearing above our page title:

Nav

Let’s add the <Layout> component to the other three pages.

// pages/about.js
import React from "react";
import Layout from "../components/Layout";

const About = () => (
  <Layout>
    <h1>About</h1>
  </Layout>
);

export default About;
// pages/blog.js
import React from "react";
import Layout from "../components/Layout";

const Blog = () => (
  <Layout>
    <h1>Blog</h1>
  </Layout>
);

export default Blog;
// pages/contact.js
import React from "react";
import Layout from "../components/Layout";

const Contact = () => (
  <Layout>
    <h1>Contact</h1>
  </Layout>
);

export default Contact;

If we head back to the browser, we can now click each navigation item and see it’s respective page content.

And while this works, you’ll notice a re-render on each page. This is because we’re using the <a> tag to link between pages, and this forces a re-render.

As a solution, Gatsby provides a <Link> component to handle page routing.

Let’s head back to Nav.js and fix the page routing.

First let’s import Link.

// Nav.js
import React from "react";
import { Link } from "gatsby";

Next, let’s replace all <a> tags with <Link> and change the href= attributes to to= attributes.

// Nav.js
import React from "react";
import { Link } from "gatsby";

const Nav = () => (
  <nav>
    <ul>
      <li>
        <Link to="/">Home</Link>
      </li>
      <li>
        <Link to="/about">About</Link>
      </li>
      <li>
        <Link to="/blog">Blog</Link>
      </li>
      <li>
        <Link to="/contact">Contact<Link>
      </li>
    </ul>
  </nav>
);

export default Nav;

Switching back to the browser, the page routing should be instantaneous when we click a navigation item.

Styling

Now that our app works, let’s add a bit of styling to make it look nicer.

Gatsby makes it simple to add specific styles when a navigation link is active.

There are two ways to add styles to an active link:

<Link activeStyle={{ backgroundColor: 'red' }}>My link</Link>
<Link activeClassName="active-link">My link</Link>

I generally use activeClassName if I have multiple links, however we’ll use activeStyle here to demonstrate CSS-in-JS.

I’ve also added some className attributes to the JSX code which we’ll use to style the rest of our navigation.

// Nav.js
import React from "react";
import { Link } from "gatsby";

import "./nav.css";

const Nav = () => (
  <nav className="nav">
    <ul className="nav-list">
      <li className="nav-list-item">
        <Link
          activeStyle={{ borderBottom: "2px solid #a64ac9" }}
	        to="/">
          Home
        </Link>
      </li>
      <li className="nav-list-item">
        <Link  
          activeStyle={{ borderBottom: "2px solid #a64ac9" }}
          to="/about">
          About
       </Link>
      </li>
      <li className="nav-list-item">
        <Link  
          activeStyle={{ borderBottom: "2px solid #a64ac9" }}
          to="/blog">
          Blog
        </Link>
      </li>
      <li className="nav-list-item">
        <Link  
          activeStyle={{ borderBottom: "2px solid #a64ac9" }}
          to="/contact">
          Contact
        </Link>
      </li>
    </ul>
  </nav>
);

export default Nav;

Now each link, when selected, will have an underline of 2px.

Inside the nav.css file in the components/ directory and add the following code.

/* nav.css */
.nav {
  padding: 24px;
}

.nav-list {
  list-style: none;
  display: flex;
  margin: 0;
  padding: 0;
}

.nav-list-item {
  margin-right: 24px;
  font-family: -apple-system, BlinkMacSystemFont,  "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell,  "Open Sans",  "Helvetica Neue",  sans-serif;
  font-size: 1.5em;
}

.nav-list a {
  color: #a64ac9;
  text-decoration: none;
  border-bottom: 2px transparent;
  transition: border 0.1s linear;
}

.nav-list a:hover,
.nav-list a:focus  {
  border-bottom: 2px solid #a64ac9;
}

Don’t forget to import nav.css in Nav.js:

// Nav.js
...
import "./nav.css";
...

Layout Styling

Now let’s add some styling to Layout.js. Create layout.css in the components/ directory.

/* layout.css */
html,
body {
  padding: 0;
  margin: 0;
  overflow-x: hidden;
}

.main {
  padding: 24px;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
    Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
  background-color: #a64ac9;
  height: 100%;
  color: #ffffff;
}

.main h1 {
  font-size: 5em;
}

.main p {
  font-size: 2em;
}

Import the CSS file into Layout.js and add a class name of layout to the outer <div> element and a class name of main to the <main> element.

// Layout.js
...
import "./layout.css";
...
<div className="layout">
  <Nav />
  <main className="main">{ children }</main>
</div>
...

Filler Content

Lastly I’m going to add some filler content to index.js, about.js, and contact.js. I added 5 paragraphs with lorem ipsum text to each of the three pages.

// index.js, about.js, contact.js
...
<p>
  Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea dignissimos
  aut consequuntur aspernatur corrupti ratione, odit similique tenetur
  accusantium, est nostrum esse minus iure voluptatum nihil cumque
  blanditiis non? Odit.
</p>
...

Your site should now look much better!

Styling

Blogs

Now it’s time to add some blogs!

Adding Blog Posts

Inside of the pages/ directory, create three folders: 2020-01-01-my-first-blog, 2020-02-14-valentines-day, 2020-04-01-april-fools.

Inside each of these folders, add an index.md file with the following structure:

---
path: '/my-first-blog'
date: '2020-01-01'
title: 'My First Blog'
author: 'Emma Bostian'
description: 'This is my very first blog of 2020!'
---

Here is my main content
It is very interesting.

Lorem ipsum dolor sit amet consectetur adipisicing elit. Ea dignissimos
aut consequuntur aspernatur corrupti ratione, odit similique tenetur
accusantium, est nostrum esse minus iure voluptatum nihil cumque
blanditiis non? Odit.

Everything between the three hyphens is called frontmatter. Frontmatter is essentially metadata for your blog post.

For each blog post, add some frontmatter, containing the following data:

Everything after the closing hyphens is the main body of the blog post. You can add whatever you’d like here.

Add markdown content for each of our three blog posts.

Rendering A List Of Blog Posts

Now that we have markdown files, we want to render them on our blog.js page.

We first need to install a dependencies:

In your terminal, run the following command:

yarn add gatsby-transformer-remark

Then, in gatsby-config.js, add gatsby-transformer-remark to the list of plugins.

  // gatsby-config.js
  plugins: [
    ...
    `gatsby-transformer-remark`,
    ...
  ]

We also need to add another plugin for gatsby-source-filesystem to tell GraphQL where to find our blog posts: our pages/ directory.

// gatsby-config.js
{
  plugins: [
    ... 
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        name: `pages`,
        path: `${__dirname}/src/pages`,
      },
    }
    ...
  ]
}

Restart your development server, then head over to http://localhost:8000/___graphql.

Gatsby comes pre-configured with GraphQL which means we have access to GraphiQL.

To get a list of all blog posts, we’ll use the AllMarkdownRemark plugin. Select the following options in the Explorer panel:

AllMarkdownRemark > edges > node > frontmatter > date title

Then press the play button.

You should see your blog post data in the right-hand panel.

All blogs

Copy this GraphQL query and head over to blog.js.

First, import graphql from gatsby.

// blog.js
...
import { graphql } from "gatsby";
...

Then, after the blog export, add the following code, pasting in the GraphQL query we just copied from GraphiQL where it says <<Your code here>>

export const AllBlogsQuery = graphql`
  <<your code here>>
`

My query looks like this (I added description path, and author to the list of data to retrieve from the frontmatter).

// blog.js
export const AllBlogsQuery = graphql`
  query AllBlogPosts {
    allMarkdownRemark {
      edges {
        node {
          frontmatter {
            date
            title
            description
            author
            path
          }
        }
      }
    }
  }
`

The last thing we have to do is pass data from the query as a parameter to the blog page. Let’s console log it to see if it’s working.

// blog.js
...
const Blog = ({ data }) => (
  <Layout>
    <h1>Blog</h1>
    { console.log(data) }
  </Layout>
);
...

Console

Dynamically Creating A List Of Blogs

Now let’s iterate over our blog data and create nodes for each of them.

Create two new files in the component folder called Post.js and post.css.

Post will take five arguments:

// Post.js
import React from "react";
import { Link } from "gatsby";
import "./post.css";

const Post = ({ title, author, date, description, path }) => (
  <div className="post">
    <h3 className="post-title">{ title }</h3>
    <p className="post-description">{ description }</p>
    <p className="post-written-by">
      Written by { author } on { date }
    </p>
    <Link to={ path }>Read more</Link>
  </div>
);

export default Post;

Here is the styling for our blog posts:

/* post.css */
.post {
  margin-bottom: 80px;
  padding-bottom: 80px;
  border-bottom: 2px solid white;
}

.post .post-title {
  font-size: 3em;
  margin: 0;
}

.post .post-description {
  font-size: 1.5em;
}

.post .post-written-by {
  font-size: 1em;
}

.post a {
  background-color: white;
  color: #a64ac9;
  padding: 16px 24px;
  text-decoration: none;
  margin-top: 16px;
  display: inline-block;
}

Now we can import Post into blog.js and render a new post for each markdown file:

// blog.js
...
import Post from "../components/Post";
...

const Blog = ({ data }) => (
  <Layout>
    <h1>Blog</h1>
    { 
      data.allMarkdownRemark.edges.map(post => {
        const { title, author, date, description, path } = post.node.frontmatter;

        return (
          <Post
            title={title}
            author={author}
            date={date}
            description={description}
            key={`${date}__${title}`}
            path={path}
          />
        )
      })
    }
  </Layout>
);

Your blog should now look like this:

Posts

Dynamically Generating Individual Blog Pages

Now that we’ve generated a list of blogs on our blog page, how can we create a page for each blog which appears when the user clicks ‘Read More’?

We could manually create a page for each post, but this would be tedious.

Luckily Gatsby, in combination with node.js, provides functionality for dynamically generating pages.

Let’s first create the GraphQL query for retrieving data for an individual blog post.

When we created the GraphQL schema for retrieving all blog posts, we used the allMarkdownRemark plugin.

This time, we only want the data for an individual blog post, so we’ll use the markdownRemark plugin.

In the Explorer panel on the left, select:

`markdownRemark > frontmatter(purple) > path > eq: "_"

Be sure to select the purple frontmatter for this part of the query; it’s an argument versus a field name.

This tells GraphQL that we will select a specific asset by their path, which will be passed as a parameter.

For this individual post we want to get several pieces of frontmatter data, so we’ll select

markdownRemark > html, frontmatter (blue) > author date path title

Be sure to select the blue frontmatter for this part of the query; it’s a field name versus an argument.

Next, where we’re passing an argument to markdownRemark, we have to give GraphQL an eq value to look for. In our case, we’ll pass the path for the blog post whose content we want to display.

We first need to pass this argument to our query, before we can pass it to the markdownRemark plugin. You can also change the name of the query to be more semantic:

query BlogPost($path: String!) {
  markdownRemark(frontmatter: { path: eq: $path }}) {
    frontmatter {
      author
      date
      title
      path
    }
    html
  }
}

String! tells GraphQL that the path argument we’re passing is of type String and is required.

Now let’s test if this query actually works.

If we press play, we get an error:

Error

This is due to the fact that our query expects an argument but we haven’t passed it one!

Open the panel at the bottom called Query Variables and enter the following:

{
  "path": "/april-fools"
}

When we press the play button now we get the data back for our April Fools blog.

Post

Now that we have our query, what do we do with it?

First, let’s build a template which will denote how each blog post should be structured.

In the src/ directory, create a new folder called templates and add a file inside called blogTemplate.js.

We first need to include some imports:

// templates/blogTemplate.js
import React from "react";
import { graphql, Link } from "gatsby";

import Layout from "../components/Layout";

Next, let’s create the function skeleton:

// templates/blogTemplate.js
...
export default function Template({ data }) {
  return ()
};

This template will take in our blog data and render it accordingly.

Finally, let’s add our GraphQL query to the bottom of the file.

// templates/blogTemplate.js
...
export const postQuery = graphql`
  query BlogPost($path: String!) {
    markdownRemark(frontmatter: { path: { eq: $path }}) {
      frontmatter {
        author
        date
        title
        path
      }
      html
    }
  }
`;

Now let’s construct our blog post structure. First, let’s grab the post and the title, author, and date from the data.

// templates/blogTemplate.js
...
export default function Template({ data }) {
  const post = data.markdownRemark;
  const { title, author, date } = post.frontmatter;
...

We’ll wrap our JSX in the <Layout> component, and inside we’ll have:

// templates/blogTemplate.js
...
export default function Template({ data }) {
  const post = data.markdownRemark;
  const { title, author, date } = post.frontmatter;
  
  return (
    <Layout>
      <Link to="/">Back to blogs</Link>
      <h1>{title}</h1>
      <p>Posted by {author} on {date}</p>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </Layout>
  )
};
...

Here is the completed blogTemplate.js file:

// templates/blogTemplate.js
import React from "react";
import { graphql, Link } from "gatsby";

import Layout from "../components/Layout";

export default function Template({ data }) {
  const post = data.markdownRemark;
  const { title, author, date } = post.frontmatter;
  
  return (
    <Layout>
      <Link to="/">Back to blogs</Link>
      <h1>{title}</h1>
      <p>Posted by {author} on {date}</p>
      <div dangerouslySetInnerHTML={{ __html: post.html }} />
    </Layout>
  )
};

export const postQuery = graphql`
  query BlogPost($path: String!) {
    markdownRemark(frontmatter: { path: { eq: $path }}) {
      frontmatter {
        author
        date
        title
        path
      }
      html
    }
  }
`;

Now that we have our template, let’s use it! We have to tell Gatsby to dynamically generate pages for each blog post, so let’s head over to gatsby-node.js.

Let’s first require the path module:

// gatsby-node.js
const path = require('path');

We’re going to use the exports.createPages API to dynamically generate our pages.

// gatsby-node.js
...
exports.createPages = ({ boundActionCreators, graphql }) => {
  const { createPages } = boundActionCreators;

  const postTemplate = path.resolve('src/templates/blogTemplate.js');
}

We now have to return a query to get all blog posts, so we can iterate over an generate our pages. We already have this query from a previous step, and all we need for each post is its path.

// gatsby-node.js
  ...
  return graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              path
            }
          }
        }
      }
    }
  `)

Once we receive a response back from the query, we want to reject the promise if an error occurred, and otherwise create a page for each post.

This will create a post at the designated path received from the query results, and will use the postTemplate we declared above (our blogPost.js template) to render each post.

// gatsby-node.js
  ... 
  return graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              path
            }
          }
        }
      }
    }
  `).then(res => {
    if (res.errors) { return Promise.reject(res.errors) }

    res.data.allMarkdownRemark.edges.forEach(({ node }) => {
    createPage({
      path: node.frontmatter.path,
      component: postTemplate
    })
  })

Here is the completed gatsby-node.js file:

// gatsby-node.js
const path = require("path");

exports.createPages = ({ boundActionCreators, graphql }) => {
  const { createPage } = boundActionCreators;
  const postTemplate = path.resolve("src/templates/blogTemplate.js");

  return graphql(`
    {
      allMarkdownRemark {
        edges {
          node {
            frontmatter {
              path
            }
          }
        }
    }
  }
  `).then(res => {
    if (res.errors) {
      return  Promise.reject(res.errors)
    }
    res.data.allMarkdownRemark.edges.forEach(({ node }) => {
      createPage({
        path: node.frontmatter.path,
        component: postTemplate,
      })
    })
  })
};

Now we’re ready to see if it worked!

Re-start your development server, then head over to the browser and click one of the blog post “Read more” links:

I’m going to add a bit more styling to make it look nicer.

I’ll create a blogTemplate.css file in the templates/ directory:

/* blogTemplate.css */
.blogTemplate .blogTemplate-title {
  margin: 80px 0 24px;
}

.blogTemplate .blogTemplate-posted-by {
  font-size: 1.2em;
}

.blogTemplate a {
  color: #fff;
}

Then I’ll import the CSS file into blogTemplate.js and add the appropriate class names. I’ll also wrap the JSX inside of <Layout> in a <div> so we can give it a class name of blogTemplate.

// templates/blogTemplate.js
import React from "react";
import { graphql, Link } from "gatsby";

import Layout from "../components/Layout";

import "./blogTemplate.css";

export default function Template({ data }) {
  const post = data.markdownRemark;
  const { title, author, date } = post.frontmatter;
  
  return (
    <Layout>
      <div className='blogTemplate'>
        <Link to="/blogs">Back to blogs</Link>
        <h1 className="blogTemplate-title">{ title }</h1>
        <p className='blogTemplate-posted-by'>Posted by { author } on { date }</p>
        <div dangerouslySetInnerHTML={{ __html: post.html }} />
      </div>
    </Layout>
  )
};

export const postQuery = graphql`
  query BlogPost($path: String!) {
    markdownRemark(frontmatter: { path: { eq: $path }}) {
      frontmatter {
        author
        date
        title
        path
      }
      html
    }
  }
`;

Your blog post should look like this:

Blog Final

Deploying To Netlify

Now that we have a working blog, let’s deploy it to Netlify!

Setting Up A GitHub Repository

First, we need to establish our blog as a Git repository.

On GitHub, create a new repo:

Repo

With the terminal, in the project directory run the following commands:

git init
git add .
git commit -m "Adding my blog files"
git remote add origin <<repo-link>>
git push -u origin master

Deploying To Netlify

Once your code is on GitHub, it’s time to deploy!

Create an account on Netlify or sign in.

Click “New site from Git” and authenticate with GitHub.

Select your newly-created repository and click “Deploy.”

Netlify

Every time you push to the master branch, your site will auto-deploy (you can change the deploy configuration but this is the default.)

You can even add a custom domain to really make the blog your own.

Conclusion

And that’s it! I hope this was helpful to explain the process for building a blog with Gatsby.

This process is tedious, so once you understand the architecture I would recommend using the Gatsby blog starter.

Feel free to contact me on Twitter if you have any questions.

Happy blogging!

Learn JavaScript the right way.

The most complete guide to learning JavaScript ever built.
Trusted by 82,951 students.

Todd Motto

with Todd Motto

Google Developer Expert icon Google Developer Expert

Free eBooks:

Angular Directives In-Depth eBook Cover

JavaScript Array Methods eBook Cover

NestJS Build a RESTful CRUD API eBook Cover