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.
Table of contents
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.
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.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- 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.
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:
components/
: Contains all of your React components (i.e. navigation).pages/
: Contains all pages with unique routes: any JavaScript file located in this directory will be accessible through its own URLmy-website/<<page-name>>
images/
: Contains all image assets for our project.
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:
- Home
- About
- Blog
- Contact
Let’s create a JavaScript file for each of these pages inside of the pages
directory:
index.js
about.js
blog.js
contact.js
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:
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!
Navigation
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:
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:
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.
Active Link Styling
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:
activeStyle
: Uses inline CSS-in-JS to style an element when active.
<Link activeStyle={{ backgroundColor: 'red' }}>My link</Link>
activeClassName
: Gives the link element a class name when active.
<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
.
Navigation Styling
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!
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:
path
: The URL path to your blogdate
: The date of publishtitle
: The blog post titleauthor
: The blog post authordescription
: The blog post description
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.
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>
);
...
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:
title
author
description
date
path
// 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:
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:
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.
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:
- A link back to the blogs page
- The blog post title
- The posted by with author name and date
- A
<div>
containing thedangerouslySetInnerHTML
attribute, which takes thepost.html
markup as the value.
// 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:
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:
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.”
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!