Hi there! I'm Ruben πŸ‘‹

I was born in πŸ‡ͺπŸ‡Έ but live in πŸ‡¦πŸ‡Ί and work for Dropbox πŸ‘¨πŸ»β€πŸ’»

Gatsby Setup - Part 1
9 min read
Β·
April 24, 2018

It's not a secret that I think Gatsby is awesome - and so fast! If you're into React development and are pro-static generators, I can't recommend it enough.

I would recommend to start with the tutorial, it's super comprehensive. Once you're done, feel free to dive into the plugins ecosystem or check those starters and templates available which are a great foundation for most needs.

However, a couple of people have asked me recently what is the Gatsby setup for this website so I thought it would be useful sharing it here too.

Goals

Before any setup it is important to understand what is the goal of your website. In my case, pinwi.co is used for the following purposes:

  • To showcase some (selected) pet projects I worked in the past or that are currently occupying my time
  • To have a blog where I can write from time to time (I was pretty active in the past and stopped, removing all the contents in the process; trying to get into the swing of things again)
  • In case any project has extra info/content and requires it's own location, to accommodate for that

As it's fairly simple and I like to play around I decided to build this one from scratch (because, you know). The objective was trying to keep Gatsby's setup as minimal as possible to cover those needs and evolve it over time as new requirements come.

Folder structure

Nothing groundbreaking but important for future's setup reference. The src/ folder has the following contents:

src
β”œβ”€β”€ components    // Reusable React Components
β”‚   └── ...js
β”œβ”€β”€ layouts       // One single layout
β”‚   β”œβ”€β”€ fonts
β”‚   β”‚   └── ...
|   β”œβ”€β”€ index.js
|   └── index.sass
β”œβ”€β”€ pages
|   β”œβ”€β”€ posts       // Blog posts
|   |   └── ...
|   β”œβ”€β”€ projects    // Projects markdown files
|   |   └── ...md
|   β”œβ”€β”€ wayne       // Wayne's project specific pinwi.co/wayne site
|   |   └── ...md
|   β”œβ”€β”€ 404.js
|   β”œβ”€β”€ blog.js       // Will render pinwi.co/blog/
|   └── index.js      // Will render pinwi.co
└── templates
    β”œβ”€β”€ blogTags.js // Blog tags template > pinwi.co/tags/xyz
    β”œβ”€β”€ posts.js    // Blog post templates > pinwi.co/blog/post
    └── wayne.js    // Wayne's project template

Plugins

To achieve the stated above I currently only use four plugins, nothing else!

In other projects I have used Contentful as the content source but I'll address that in a different post.

As such, gatsby-config.js plugins configuration is as follows:

plugins: [
  'gatsby-plugin-react-helmet',
  'gatsby-plugin-sass',
  'gatsby-transformer-remark',
  {
    resolve: `gatsby-source-filesystem`,
    options: {
      path: `${__dirname}/src/pages/projects`,
      name: 'projects',
    },
  },{
    resolve: `gatsby-source-filesystem`,
    options: {
      path: `${__dirname}/src/pages/posts`,
      name: 'posts',
    },
  },{
    resolve: `gatsby-source-filesystem`,
    options: {
      path: `${__dirname}/src/pages/wayne`,
      name: 'wayne',
    },
  }
]

As you can see, there are three different entries for gatsby-source-filesystem to accommodate to the three different types of content. The options.name entry is what will be used later on to create the pages for each one of them.

In a similar fashion, if I start a new project in the future that needs its own space I would need to add a new entry:

{
  resolve: `gatsby-source-filesystem`,
  options: {
    path: `${__dirname}/src/pages/xyz`,
    name: 'xyz',
  },
}

Creating pages

I really didn't start using GraphQL until I started playing with Gatsby and I have to say it took me a bit to get the swing of things but I now appreciate its flexibility for fetching data.

Regarding the pages creation, all the magic happens in gatsby-node.js. There are 3 different queries to create the different page types in this website. As an example, let's look at the code for creating the blog posts pages.

To start, we have the query that retrieves all blog posts:

// Blog posts
graphql(`{
 allFile(filter: {sourceInstanceName: {eq: "posts"}}) {
    edges {
      node {
        childMarkdownRemark {
          id
          frontmatter {
            title
            path
            tags
          }
        }
      }
    }
  }
}`).then(result => {
  if (result.errors) {
    return Promise.reject(result.errors)
  }

  ...
}

I'm using allFile instead of allMarkdownRemark to be able to filter by the source name created in gatsby-config. In that, for the other two types of contents I just need to swap the sourceInstanceName's filter for its specific value and define which values frontmatter should return.

I found this to be simpler and cleaner than other alternatives.

I am also collecting childMarkdownRemark.id as it will be the value to pass to the page creation later on.

If the query is successful, there are to types of pages that are created.

First, the pages for each post with:

  • its desired path, which always starts with /blog/ and concatenates the slug defined in the post
  • the blog's post template
  • and passing childMarkdownRemark.id as the context to the template
// Inits
const blogPosts = result.data.allFile.edges
let tags = []

// Posts pages creation
blogPosts.forEach(tmp => {
  let post = tmp.node.childMarkdownRemark.frontmatter
  createPage({
    path: `/blog/${post.path}/`,
    component: blogTemplate,
    context: {
      id: tmp.node.childMarkdownRemark.id
    },
  })
  // Appending tags for future use
  tags = tags.concat(post.tags)
})

Lastly, I remove all duplicated tags and create a page for each one of them.

// Eliminate duplicate tags
tags = _.uniq(tags)

// Create blog tags pages
tags.forEach(tag => {
  createPage({
    path: `/blog/tags/${_.kebabCase(tag)}/`,
    component: blogTagsTemplate,
    context: {
      tag,
    },
  })
})

And we're done for the blog posts! Pretty simple.

The projects follow a similar approach, while for Wayne's project I don't need any tag pages so less work on that end.

Now to the cool stuff - i.e., the front-end.

Styling

If you want to use Bulma inside Gatsby you just need to follow some simple steps:

  • Install Bulma's dependencies via npm
  • Create a src/layouts/index.saas file with the following content
@import "~bulma/sass/utilities/initial-variables"

// Edit your Bulma variables here

@import "~bulma"

// Any extra styling goes here
  • Include it in src/layouts/index.js,
import React from 'react'
import PropTypes from 'prop-types'
import Helmet from 'react-helmet'

import './index.sass'
import './fonts/ionicons/scss/ionicons.scss'
import './fonts.css'

As you can see I also have a src/layouts/fonts folder with those fonts I'm using, such as ionicons.

Bringing the content to life

Following the blog posts as our example, there are three items needed to present the content:

  • A page to display an excerpt for all the posts: src/pages/blog.js
  • A template to display an except for all posts for each tags: src/templates/blogTags.js
  • A template to display the post itself: src/templates/post.js

For the first two, I am using the same GraphQL query:

allMarkdownRemark(
  filter: {frontmatter: {type: {eq: "post"}}},
  sort: { order: DESC, fields: [frontmatter___date] }
) {
    edges {
      node {
        excerpt(pruneLength: 250)
        id
        frontmatter {
          title
          date(formatString: "MMMM DD, YYYY")
          path
        }
      }
    }
  }

In this case allMarkdownRemark allows us to filter by the -formatter.type_ and sort by the post date. That is possible because all posts markdown files start with the following header:

---
path: "gatsby-setup-part-1"
type: "post"
date: "2018-04-23"
title: "Gatsby Setup - Part 1"
tags: ["gatsby","code","react"]
---

The remaining bit for src/pages/blog.js is to render the content:

export default class BlogPage extends React.Component {
  render() {
    const { data } = this.props
    const { edges: posts } = data.allMarkdownRemark;
    return (
      <div className="section columns is-centered">
        <div className="column is-10">
          {posts
            .filter(post => post.node.frontmatter.title.length > 0)
            .map(({ node: post }) => (
                <Excerpt post={post} />
              )
            )}
        </div>
      </div>
    )
  }
}

I'm also avoiding rendering posts without title (i.e., potential drafts).

The same applies for the tag pages but in this case there's a breadcrumb that serves as a subtle navigation and an extra filtering in place to validate whether that post includes the tag in question (the GraphQL query of this class also fetches frontmatter.tags).

export default class BlogTagsTemplate extends React.Component {
  render() {
    const { pathContext, data } = this.props
    const { edges: posts } = data.allMarkdownRemark;
    const { tag } = pathContext ? pathContext : null
    return (
      <div className="section columns is-centered">
        <div className="column is-10">
          <nav className="breadcrumb" aria-label="breadcrumbs">
            <ul>
              <li><Link to="/blog/">All posts</Link></li>
              <li className="is-active"><a>{tag}</a></li>
            </ul>
          </nav>
          {posts
            .filter(post => post.node.frontmatter.title.length > 0)
            .filter(post => post.node.frontmatter.tags.includes(tag))
            .map(({ node: post }) => {
              return (
                <Excerpt post={post} />
              );
            })}
        </div>
      </div>
    )
  }
}

Finally, the only remaining bit is src/templates/post.js to render the post itself.

In this template the GraphQL receives the id of the post to fetch using markdownRemark.

export default class PostTemplate extends React.Component {
  render() {
    const { pathContext, data } = this.props
    const post = data.markdownRemark
    return (
      <div className="section">
        <div className="columns is-centered">
          <div className="column is-10">
            <div className="level">
              <div className="level-left">
                <div className="level-item title is-3">{post.frontmatter.title}</div>
              </div>
              <div className="level-right">
                <div className="level-item subtitle is-7">{post.frontmatter.date}</div>
              </div>
            </div>
            <div className="content" dangerouslySetInnerHTML={{ __html: post.html }} />
            <div className="section">
              <div className="level">
                <div className="level-left">
                  {post.frontmatter.tags.map(tag => (
                    <Link to={"/blog/tags/"+tag} className="level-item tag is-success">
                      {tag}
                    </Link>
                  ))}
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    )}
}


export const PostTemplateQuery = graphql`
  query PostTemplateQuery ($id: String!){
    markdownRemark (id: {eq: $id}) {
        html
        frontmatter {
          date(formatString: "MMMM DD, YYYY")
          title
          tags
        }
    }
}
`

And we're done!

That's it! The only remaining bit is to build and deploy the website. In my case, I like more Firebase over Github Pages, Netlify or others but that's a personal preference.

In future entries I might cover how to automate Continuous Integration/Delivery or using a different source of content, critical for pages where there are multiple collaborators.

Hope if was useful. If you have any questions, feel free to send me a tweet!