Development · May 2019

Advanced blog system in Gatsby

Create a complete blog from scratch in Gatsby with pagination, categories, featured post, author, SEO and navigation.

Danilo WoznicaDeveloper
Photo by Frederico Jesus
Photo by Frederico Jesus

👉 Check out the demo and source.

What’s Gatsby.js?

Today, static site generators are one of the most popular ways to build websites. You get a complete build done quickly without complications, hosted cheaply or even for free. That’s why the community started creating different ways to build static site generators

The React community has a couple of tools/frameworks that can generate a static website. My favorite, and yours probably too, is Gatsby: “a free and open-source framework based on React that helps developers build blazing-fast websites and apps.”

🛠 What will we build?

I suppose you know enough about Gatsby, so I won’t go into detail on how basic things like query/StaticQuey, layout, and pages work. My goal here is to show you how I implemented a complete blog “from scratch” only using gatsby-node.js and its pageContext API.

So, let’s try to solve the following problems:

  • Pagination;
  • Category and tag pages (with pagination);
  • Category list (with navigation);
  • Featured post;
  • Author page;
  • Next and previous post;
  • SEO component.

✍️ Data structure

To organize the data, I normally create a folder in the root project called /content where I put all the files related to the content of my website. Then I usually create another folder named /content/blog which will be the base folder to write every blog post. Even though that is the way I prefer to work, feel free to choose what is better for you.

So for each new blog post, you need to create a new folder with the title slugified (or with any other name as long as it’s unique). Inside it there will be an index.md and every other static file that you’re going to use in the blog post.

After that, your project should look something like this:

1... default Gatsby files
2|-- content
3 | |-- blog
4 | |-- my-first-blog-post
5 | | |-- index.md
6 | | |-- my-image.jpeg
7 | |-- my-second-blog-post
8 | |-- index.md
9 | |-- my-another-image.jpeg

Now let’s take a look at how each .md file looks like, especially in the frontmatter:

1---
2title: My first blog post
3date: 2019-01-21
4author: Danilo Woznica
5featured: true
6image: ./my-image.jpeg
7category:
8 - News
9 - Events
10tags:
11 - Portugal
12 - Porto
13---
14
15Lorem ipsum dolor amet helvetica cardigan readymade wayfarers cold-pressed poutine. Enamel pin polaroid gluten-free helvetica single-origin coffee. Marfa cold-pressed williamsburg taxidermy Kickstarter semiotics tote bag heirloom gastropub. Quinoa pop-up brunch, vice hashtag biodiesel selfies affogato meditation pork pok heirloom chillwave yr meh marfa. Direct trade poke try-hard, raclette pok pok af succulents tbh keffiyeh four dollar toast pork belly ramps squid.

Some things worth mentioning:

  • Category and tag fields are an array of string (you will use this later);
  • The path of the image is relative to the file;
  • The featured field is a boolean and you also will handle it later.

So that’s the basic structure of your blog post. Inside this file, you will write your content but first let’s go ahead and know how the application will consume the content.

🤝 How does Gatsby meet the content?

First of all, we need to tell Gatsby where your content is and what it needs to do with these files. So let’s open the gatsby-config.js file and install a few plugins:

1module.exports = {
2 ...
3 plugins: [
4 ...
5 {
6 resolve: `gatsby-source-filesystem`,
7 options: {
8 path: `${__dirname}/content/blog`,
9 name: `blog`,
10 },
11 },
12 {
13 resolve: `gatsby-transformer-remark`,
14 options: {
15 plugins: [
16 `gatsby-remark-images`,
17 ],
18 },
19 },
20 ],
21}

These plugins are:

* Don’t forget to install them and add those in the package.json as dev dependencies.

⚙️ Creating the pages

Probably you know that Gatsby uses graphql to consume the content and create its pages. So let’s skip this step and let me present you the interesting API of gatsby-node.js. If you have no idea what I’m talking about, please take your time to look at the documentation about graphql in Gatsby.

Querying the content

Now if you take a look at the graphql playground (http://localhost:8000/___graphql) and query the content using the schema called allMarkdownRemark you can see the content you’ve just created. Besides that, you can filter, sort and even skip the content. But for now, you will use one of those: the sort.

So the final query looks like this:

1query blogPosts {
2 allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
3 edges {
4 node {
5 frontmatter {
6 title
7 category
8 date
9 }
10 html
11 }
12 }
13 }
14}

Creating the pages programmatically

Now open your gatsby-node.js or create it in the root folder if you don’t have this file. And let’s write a function which will: query the content, look to results, pass-through for each blog post and create a new page using a custom layout.

But first of all, we need to define how the blog post URL should be. I prefer to use a WordPress friendly way, like /blog/YEAR/MONTH/DAY/TITLE-SLUG:

1const { createFilePath } = require(`gatsby-source-filesystem`)
2
3exports.onCreateNode = ({ node, actions, getNode }) => {
4 const { createNodeField } = actions
5
6 if (node.internal.type === `MarkdownRemark`) {
7 const value = createFilePath({ node, getNode })
8 const [month, day, year] = new Date(node.frontmatter.date)
9 .toLocaleDateString('en-EN', {
10 year: 'numeric',
11 month: '2-digit',
12 day: '2-digit',
13 })
14 .split('/')
15 const slug = value.replace('/blog/', '').replace(/\/$/, '')
16 const url = `/blog/${year}/${month}/${day}${slug}`
17 createNodeField({
18 name: `slug`,
19 node,
20 value: url,
21 })
22 }
23}

Putting this code in gatsby-node.js, every markdown node will have a field called slug which has the path to the blog post.

From now on you can query the content and create all the pages of the blog posts, which you can do by using the following code in the same file:

1const path = require(`path`)
2
3// 1. This is called once the data layer is bootstrapped to let plugins create pages from data.
4exports.createPages = ({ graphql, actions }) => {
5 // 1.1 Getting the method to create pages
6 const { createPage } = actions
7 // 1.2 Tell which layout Gatsby should use to thse pages
8 const blogLayout = path.resolve(`./src/layouts/blog-post.js`)
9
10 // 2 Return the method with the query
11 return graphql(`
12 query blogPosts {
13 allMarkdownRemark(sort: { fields: [frontmatter___date], order: DESC }) {
14 edges {
15 node {
16 fields {
17 slug
18 }
19 frontmatter {
20 title
21 date
22 author
23 category
24 tags
25 featured
26 }
27 html
28 }
29 }
30 }
31 }
32 `).then(result => {
33 // 2.1 Handle the errors
34 if (result.errors) {
35 console.error(result.errors)
36 reject(result.errors)
37 }
38
39 // 2.2 Our posts are here
40 const posts = result.data.allMarkdownRemark.edges
41
42 // 3 Loop throught all posts
43 posts.forEach((post, index) => {
44 // 3.1 Finally create posts
45 createPage({
46 path: post.node.fields.slug,
47 component: blogLayout,
48 context: {
49 slug: post.node.fields.slug,
50 },
51 })
52 })
53 })
54}

Run the Gatsby and you can see that you’ve just created the pages programmatically only using .md files.

📃 Single post page

Do you remember that in the createPages you referred a layout? At this moment let’s show the content of a single post on the page.

To do that, you need to use the pageContext (which are some variables from Gatsby) and query the content filtering by the context you just passed when you created the pages, a few steps ago.

The simplest way to load the blog post would be something like:

1import React from "react"
2import { graphql } from "gatsby"
3
4const BlogPost = ({ data }) => {
5 const { markdownRemark } = data
6 const imageSource = markdownRemark.frontmatter.image.childImageSharp.fluid.src
7
8 return (
9 <>
10 <img src={imageSource} alt={markdownRemark.frontmatter.title} />
11 <h1>{markdownRemark.frontmatter.title}</h1>
12 <p>{markdownRemark.frontmatter.date}</p>
13 <p>By {markdownRemark.frontmatter.author}</p>
14 <p>In: {markdownRemark.frontmatter.category.join()}</p>
15 <p>Tags: {markdownRemark.frontmatter.tags.join()}</p>
16 <div dangerouslySetInnerHTML={{ __html: markdownRemark.html }} />
17 </>
18 )
19}
20
21export default BlogPost
22
23export const query = graphql`
24 query BlogPostBySlug($slug: String!) {
25 markdownRemark(fields: { slug: { eq: $slug } }) {
26 html
27 frontmatter {
28 title
29 date(formatString: "MMMM DD, YYYY")
30 author
31 category
32 image {
33 childImageSharp {
34 fluid {
35 src
36 }
37 }
38 }
39 }
40 }
41 }
42}

So easy!

📰 What about the blog post list?

In the same way, you got all the blog posts to create the pages in the gatsby-node.js, you can use the same query to get all blog posts and print it on a new page. But let’s assume that you have too many posts and this list becomes quite long, it would be cool to add some kind of pagination.

For me, the infinite scroll (which I hated) or the “Show more X posts” doesn’t make much sense for a blog, so I will show you how to create normal one pagination.

Creating a blog post list page

So instead of creating a file in the page folder (the regular way), let’s create another layout in /layouts folder and set it in the gatsby-node.js, above the blog post layout one:

1const blogListLayout = path.resolve(`./src/layouts/blog-list.js`)

Then you have to decide how many posts you would like to show on each page. In my case, I think that 9 posts are of great value. Next, let’s get the amount of blog posts and remove the featured post from this count, because probably you don’t want to show this post twice, one in the hero and in the list, right?

1const postsPerPage = 9
2const postsWithoutFeatured = posts.filter(({ node }) => {
3 return !node.frontmatter.featured
4})
5const numPages = Math.ceil(postsWithoutFeatured.length / postsPerPage)

Now, once the magic happens, you need to create an array with the same length as the number of pages, then pass through them as you create the pages:

1Array.from({ length: numPages }).forEach((_, i) => {
2 createPage({
3 path: i === 0 ? `/blog` : `/blog/page/${i + 1}`,
4 component: blogListLayout,
5 context: {
6 limit: postsPerPage,
7 skip: i * postsPerPage,
8 currentPage: i + 1,
9 numPages,
10 },
11 })
12})

Cool? Does that make sense?

If so, let’s go on.

The view part

Did you notice the keys limit and skip in the context in the last step? That will define the position of your page in the view. Then graphql will catch this info and it will only show the posts between this range.

Next, you will use this information to create the pagination component, with next and previous page, current page and navigation, by pageContext prop:

1import React from 'react'
2import { graphql, Link } from 'gatsby'
3
4const BlogPostList = ({ data, pageContext }) => {
5 const { allMarkdownRemark } = data
6
7 return (
8 <>
9 {allMarkdownRemark.edges.map(({ node }) => {
10 const imageSource = node.frontmatter.image.childImageSharp.fluid.src
11
12 return (
13 <>
14 <Link to={node.fields.slug}>
15 <img src={imageSource} alt={node.frontmatter.title} />
16 <h1>{node.frontmatter.title}</h1>
17 </Link>
18 <p>{node.frontmatter.date}</p>
19 <p>By {node.frontmatter.author}</p>
20 <p>In: {node.frontmatter.category.join()}</p>
21 </>
22 )
23 })}
24
25 <ul>
26 {Array.from({ length: pageContext.numPages }).map((item, i) => {
27 const index = i + 1
28 const link = index === 1 ? '/blog' : `/blog/page/${index}`
29
30 return (
31 <li>
32 {pageContext.currentPage === index ? (
33 <span>{index}</span>
34 ) : (
35 <a href={link}>{index}</a>
36 )}
37 </li>
38 )
39 })}
40 </ul>
41 </>
42 )
43}
44
45export default BlogPostList
46
47export const query = graphql`
48 query blogPostsList($skip: Int!, $limit: Int!) {
49 allMarkdownRemark(
50 sort: { fields: [frontmatter___date], order: DESC }
51 filter: { frontmatter: { featured: { eq: false } } }
52 limit: $limit
53 skip: $skip
54 ) {
55 edges {
56 node {
57 fields {
58 slug
59 }
60 frontmatter {
61 title
62 date
63 author
64 category
65 image {
66 childImageSharp {
67 fluid {
68 src
69 }
70 }
71 }
72 }
73 }
74 }
75 }
76 }
77`

🏷️ Getting the categories

Once you build the main page with the pagination component done, it’s quite easy to generate the categories pages. The first thing we have to do is get all categories from the blog post and count how many posts there are in each category. So let’s do it, step-by-step:

The first step is to create a new layout as you’ve already made with the blog list and blog post:

1const blogCategoryLayout = path.resolve(`./src/layouts/blog-category.js`)

Then, get all categories and save it in a new array:

1const categories = []
2
3posts.forEach((post, index) => {
4 post.node.frontmatter.category.forEach(cat => categories.push(cat))
5
6 createPage({
7 path: post.node.fields.slug,
8 component: blogLayout,
9 context: {
10 slug: post.node.fields.slug,
11 },
12 })
13})

After that, you need to know how many posts there are in each category (remember that category field in the markdown file is an array):

1const countCategories = categories.reduce((prev, curr) => {
2 prev[curr] = (prev[curr] || 0) + 1
3 return prev
4}, {})

Now you have enough data to create the pages, by category and paginated:

1const kebabCase = require(`lodash.kebabcase`)
2
3const allCategories = Object.keys(countCategories)
4
5allCategories.forEach((cat, i) => {
6 const link = `/blog/category/${kebabCase(cat)}`
7
8 Array.from({
9 length: Math.ceil(countCategories[cat] / postsPerPage),
10 }).forEach((_, i) => {
11 createPage({
12 path: i === 0 ? link : `${link}/page/${i + 1}`,
13 component: blogCategoryLayout,
14 context: {
15 allCategories: allCategories,
16 category: cat,
17 limit: postsPerPage,
18 skip: i * postsPerPage,
19 currentPage: i + 1,
20 numPages: Math.ceil(countCategories[cat] / postsPerPage),
21 },
22 })
23 })
24})

Once again, the page context is very important to you, because it will tell to graphql which category should query and show in the view. Please note that you’re passing the category field in the context above, now your view will look much similar to /blog-list.js, but with an important difference, you will filter the posts by category, which comes from the context:

1import React from 'react'
2import kebabCase from 'lodash.kebabcase'
3import { graphql, Link } from 'gatsby'
4
5const BlogCategory = ({ data, pageContext }) => {
6 const { allMarkdownRemark } = data
7
8 return (
9 <>
10 <h1>Categories:</h1>
11 {pageContext.allCategories.map(cat => (
12 <Link to={`/blog/category/${kebabCase(cat)}`}>{cat}</Link>
13 ))}
14 <br />
15
16 {allMarkdownRemark.edges.map(({ node }) => {
17 const imageSource = node.frontmatter.image.childImageSharp.fluid.src
18
19 return (
20 <>
21 <Link to={node.fields.slug}>
22 <img src={imageSource} alt={node.frontmatter.title} />
23 <h1>{node.frontmatter.title}</h1>
24 </Link>
25 <p>{node.frontmatter.date}</p>
26 <p>By {node.frontmatter.author}</p>
27 <p>
28 In:{' '}
29 {node.frontmatter.category.map(cat => (
30 <Link to={`/blog/category/${kebabCase(cat)}`}>{cat}</Link>
31 ))}
32 </p>
33 </>
34 )
35 })}
36
37 <ul>
38 {Array.from({ length: pageContext.numPages }).map((item, i) => {
39 const index = i + 1
40 const category = kebabCase(pageContext.category)
41 const link =
42 index === 1
43 ? `/blog/category/${category}`
44 : `/blog/category/${category}/page/${index}`
45
46 return (
47 <li>
48 {pageContext.currentPage === index ? (
49 <span>{index}</span>
50 ) : (
51 <a href={link}>{index}</a>
52 )}
53 </li>
54 )
55 })}
56 </ul>
57 </>
58 )
59}
60
61export default BlogCategory
62
63export const query = graphql`
64 query blogPostsListByCategory($category: String, $skip: Int!, $limit: Int!) {
65 allMarkdownRemark(
66 sort: { fields: [frontmatter___date], order: DESC }
67 filter: { frontmatter: { category: { in: [$category] } } }
68 limit: $limit
69 skip: $skip
70 ) {
71 edges {
72 node {
73 fields {
74 slug
75 }
76 frontmatter {
77 title
78 date
79 author
80 category
81 image {
82 childImageSharp {
83 fluid {
84 src
85 }
86 }
87 }
88 }
89 }
90 }
91 }
92 }
93`

Note that only two things were updated:

  • The pagination URL;
  • Query filter.

Category list

To show a list of categories in the main blog list, just pass the field allCategories as a context in the pagination creation part and you’ll receive this field in the view as an array in the pageContext.

Getting the tags

If perhaps you would like to have tags in your blog post, just repeat the same process you just did with the category.

⛓ Next and prev post

As you can imagine, to solve this issue, you need to pass a new context when you’re creating the blog post pages in gatsby-node.js. To get the next and previous posts all you need to do is catch it when you’re looping through all blog posts. Once you know that the blog posts are sorted by date, you can be sure that the next index of the array will be the next post and the previous index, the previous post, of course:

1posts.forEach((post, index, arr) => {
2 post.node.frontmatter.category.forEach(cat => categories.push(cat))
3
4 const prev = arr[index - 1]
5 const next = arr[index + 1]
6
7 createPage({
8 path: post.node.fields.slug,
9 component: blogLayout,
10 context: {
11 slug: post.node.fields.slug,
12 prev: prev,
13 next: next,
14 },
15 })
16})

And the view could be:

1const BlogPost = ({ data, pageContext }) => {
2 const { markdownRemark } = data
3 const { prev, next } = pageContext
4
5 return (
6 <>
7 ...
8 {prev && (
9 <Link to={prev.node.fields.slug}>
10 {'<'} {prev.node.frontmatter.title}
11 </Link>
12 )}
13 {next && (
14 <Link to={next.node.fields.slug}>
15 {next.node.frontmatter.title} {'>'}
16 </Link>
17 )}
18 </>
19 )
20}

Really easy, with any trick!

🔍 SEO

Last but not least, we need to update the title, descriptions and the other content of our blog for a better read by the search engine, especially on the blog post page. I’ve tried a lot of different approaches to solve this issue and, in my opinion, the way in which Gatsby does it, it’s the best I’ve seen so far. So please take a look at their documentation.

The good news here is that probably when you set up the Gatsby project, this component will be almost done. Just check if the component is following the documentation.

After everything is working fine, you can add this component into the blog post like this:

1<SEO title={markdownRemark.frontmatter.title} />

🤔 Conclusion

If you’re looking for a complete tool to develop your personal or even commercial blog in React, with server-side-render and other wonderful optimizations, I think Gatsby is the best choice, compared to the other frameworks that exist today. It’s easy to extend, it has an amazing API that places all the power in your hands. Also, Gatsby team is always releasing great updates and doing important fixes, so you’ll be headed in the right direction with the best of the React community for the static site.

But remember that it is not a tool like Wordpress. It doesn’t have an infinity of plugins to do whatever you need (well it’s almost there) and it even doesn’t have a CMS where you can manage the content like WYSIWYG.

The solution I’ve just shown in this article, requires the person who will manage the content to have a minimal knowledge about markdown and version control (git). However, if it’s a required feature maybe Prismic or Netlify CMS could solve this problem, but I haven’t yet tried these tools within this workflow. If you already have, let me know your experience and your thoughts.

👉 Check out the demo and source. 🎉

Web DevelopmentReactjsJavaScriptGatsbyjsProgramming

Danilo Woznica

Developer @ Significa

Danilo came from the Wild West across the Atlantic also described as Brazil. That’s right, he was born in the home of Football and Cachaça and somehow he chose to become a Front-End Developer. Good for us!
É noix!