Build a Modern Blog with Gatsby and MongoDB
Rate this tutorial
The web, like many other industries, works in a very cyclical way. Trends are constantly born and reborn. One of my favorite trends that's making a huge come back is static websites and focus on website performance. GatsbyJS presents a new way of building websites that mergers the static with the dynamic that in my opinion provides a worthwhile framework for your consideration.
In today's tutorial, we're going to take a look at how we can leverage GatsbyJS and MongoDB to build a modern blog that can be served anywhere. We'll dive into how GraphQL makes it easy to visualize and work with our content regardless of where it's coming from. Get the code from this GitHub repo to follow along.
For this tutorial you'll need:
You can download Node.js here, and it will come with the latest version of npm. For MongoDB, you can use an existing install or MongoDB Atlas for free. The dataset we'll be working with comes from Hakan Özler, and can be found in this GitHub repo. All other required items will be covered in the article.
The app that we are building today is called Books Plus. It is a blog that reviews technical books.
GatsbyJS is a React based framework for building highly performant websites and applications. The framework allows developers to utilize the modern JavaScript landscape to quickly build static websites. What makes GatsbyJS really stand out is the ecosystem built around it. Plugins for all sorts of features and functionality easily interoperate to provide a powerful toolkit for anything you want your website to do.
The second key feature of GatsbyJS is it's approach to data sources. While most static website generators simply process Markdown files into HTML, GatsbyJS provides a flexible mechanism for working with data from any source. In our article today, we'll utilize this functionality to show how we can have data both in Markdown files as well as in a MongoDB database, and GatsbyJS will handle it all the same.
To create a GatsbyJS site, we'll need to install the Gatsby CLI. In your Terminal window run
npm install -g gatsby-cli
.To confirm that the CLI is properly installed run
gatsby -help
in your Terminal. You'll see a list of available commands such as gatsby build and gatsby new. If you see information similar to the screenshot above, you are good to go.The next step will be to create a new GatsbyJS website. There's a couple of different ways we can do this. We can start with a barebones GatsbyJS app or a starter app that has various plugins already installed. To keep things simple we'll opt for the former. To create a new barebones GatsbyJS website run the following command:
1 gatsby new booksplus
Executing this command in your Terminal will create a new barebones GatsbyJS application in a directory called
booksplus
. Once the installation is complete, navigate to this new directory by running cd booksplus
and once in this directory let's start up our local GatsbyJS development server. To do this we'll run the following command in our Terminal window.1 gatsby develop
This command will take a couple of seconds to execute, but once it has, you'll be able to navigate to
localhost:8080
to see the default GatsbyJS starter page.The default page is not very impressive, but seeing it tells us that we are on the right path. You can also click the Go to page 2 hyperlink to see how Gatsby handles navigation.
If you were paying attention to your Terminal window while GatsbyJS was building and starting up the development server you may have also noticed a message saying that you can navigate to
localhost:8000/___graphql
to explore your sites data and schema. Good eye! If you haven't, that's ok, let's navigate to this page as well and make sure that it loads and works correctly.GraphiQL is a powerful user interface for working with GraphQL schemas, which is what GatsbyJS generates for us when we run
gatsby develop
. All of our websites content, including pages, images, components, and so on become queryable. This API is automatically generated by Gatsby's build system, we just have to learn how to use it to our advantage.If we look at the Explorer tab in the GraphiQL interface, we'll see the main queries for our API. Let's run a simple query to see what our current content looks like. The query we'll run is:
1 query MyQuery { 2 allSitePage { 3 totalCount 4 } 5 }
Running this query will return the total number of pages our website currently has which is 5.
We can add on to this query to return the path of all the pages. This query will look like the following:
1 query MyQuery { 2 allSitePage { 3 totalCount 4 nodes { 5 path 6 } 7 } 8 }
And the result:
The great thing about GraphQL and GraphiQL is that it's really easy to build powerful queries. You can use the explorer to see what fields you can get back. Covering all the ins and outs of GraphQL is out of the scope of this article, but if you are interested in learning more about GraphQL check out this crash course that will get you writing pro queries in no time.
Now that we have our app set up, let's get to building our application.
A blog isn't very useful without content. Our blog reviews books. So the first thing we'll do is get some books to review. New books are constantly being released, so I don't think it would be wise to try and keep track of our books within our GatsbyJS site. A database like MongoDB on the other hand makes sense. Hakan Özler has a curated list of datasets for MongoDB and one of them just happens to be a list of 400+ books. Let's use this dataset.
I will import the dataset into my database that resides on MongoDB Atlas. If you don't already have MongoDB installed, you can get a free account on MongoDB Atlas.
In my MongoDB Atlas cluster, I will create a new database and call it
gatsby
. In this new database, I will create a collection called books
. There are many different ways to import data into your MongoDB database, but since I'm using MongoDB Atlas, I'll just import it directly via the web user interface.Our sample dataset contains 431 books, so after the import we should see 431 documents in the books collection.
Now that we have our data, let's use it in our GatsbyJS application. To use MongoDB as a data source for our app, we'll need to install the
gatsby-source-mongodb
plug in. Do so by running1 npm install --save gatsby-source-mongodb
in your Terminal window. With the plugin installed, the next step will be to configure it. Open up the
gatsby-config.js
file. This file contains our site metadata as well as plugin configuration options. In the plugins
array, let's add the gatsby-source-mongodb
plugin. It will look something like this:1 { 2 // The name of the plugin 3 resolve: 'gatsby-source-mongodb', 4 options: { 5 // Name of the database and collection where are books reside 6 dbName: 'gatsby', 7 collection: 'books', 8 server: { 9 address: 'main-shard-00-01-zxsxp.mongodb.net', 10 port: 27017 11 }, 12 auth: { 13 user: 'ado', 14 password: 'password' 15 }, 16 extraParams: { 17 replicaSet: 'Main-shard-0', 18 ssl: true, 19 authSource: 'admin', 20 retryWrites: true 21 } 22 } 23 },
Save the file. If your
dbName
and collection
are different from the above, take note of them as the naming here is very important and will determine how you interact with the GraphQL API.If your GatsbyJS website is still running, stop it, run
gatsby clean
and then gatsby develop
to restart the server. The gatsby clean
command will clear the cache and delete the previous version. In my experience, it is recommended to run this as otherwise you may run into issues with the server restarting correctly.When the
gatsby develop
command has successfully been re-run, navigate to the GraphiQL UI and you should see two new queries available: mongodbGatsbyBooks
and allMongodbGatsbyBooks
. Please note that if you named your database and collection something different, then these query names will be different. The convention they will follow though will be mongodb<Your-Database-Name><Your-CollectionName>
and allMongodb<Your-Database-Name><Your-CollectionName>
.Let's play with one of these queries and see what data we have access to. Execute the following query in GraphiQL:
1 query MyQuery { 2 allMongodbGatsbyBooks { 3 edges { 4 node { 5 title 6 } 7 } 8 } 9 }
Your result will look something like this:
Excellent. Our plugin was configured successfully and we see our collection data in our GatsbyJS website. We can add on to this query by requesting additional parameters like the authors, categories, description and so on, but rather than do that here, why don't we render it in our website.
We want to display the book catalog on our homepage. Let's open up the
index.js
page located in the src/pages
directory. This React component represent our homepage. Let's clean it up a bit before we start adding additional styles. Our new barebones component will look like this:1 import React from "react" 2 import { Link } from "gatsby" 3 4 import Layout from "../components/layout" 5 6 const IndexPage = () => ( 7 <Layout> 8 <div className="book-container"> 9 10 </div> 11 </Layout> 12 ) 13 14 export default IndexPage
Next let's add a GraphQL query to get our books data into this page. The updated code will look like this:
1 import React from "react" 2 import { Link } from "gatsby" 3 import { graphql } from "gatsby" 4 5 import Layout from "../components/layout" 6 7 const IndexPage = () => ( 8 <Layout> 9 <div className="book-container"> 10 11 </div> 12 </Layout> 13 ) 14 15 export default IndexPage 16 17 export const pageQuery = graphql` 18 query { 19 allMongodbGatsbyBooks { 20 edges { 21 node { 22 id 23 title 24 shortDescription 25 thumbnailUrl 26 } 27 } 28 } 29 } 30 `
We are making a call to the
allMongodbGatsbyBooks
query and asking for all the books in the collection. For each book we want to get its id, title, shortDescription and thumbnailUrl. Finally, to get this data into our component, we'll pass it through props:1 import React from "react" 2 import { Link } from "gatsby" 3 import { graphql } from "gatsby" 4 5 import Layout from "../components/layout" 6 7 const IndexPage = (props) => { 8 const books = props.data.allMongodbGatsbyBooks.edges; 9 10 return ( 11 <Layout> 12 <div className="book-container"> 13 14 </div> 15 16 </Layout> 17 ) 18 } 19 20 export default IndexPage 21 22 export const pageQuery = graphql` 23 query { 24 allMongodbGatsbyBooks { 25 edges { 26 node { 27 id 28 title 29 shortDescription 30 thumbnailUrl 31 } 32 } 33 } 34 } 35 `
Now we can render our books to the page. We'll do so by iterating over the books array and displaying all of the information we requested. The code will look like this:
1 return ( 2 <Layout> 3 <div className="book-container"> 4 {books.map(book => 5 <div className="book"> 6 <Link to={'/book/' + book.node.id}> 7 <img src={book.node.thumbnailUrl} /> 8 <h2>{book.node.title}</h2> 9 <p>{book.node.shortDescription}</p> 10 </Link> 11 </div> 12 )} 13 </div> 14 </Layout> 15 )
Let's go to
localhost:8000
and see what our website looks like now. It should look something like:If you start scrolling you'll notice that all 400+ books were rendered on the page. All this data was cached so it will load very quickly. But if we click on any of the links, we will get a 404. That's not good, but there is a good reason for it. We haven't created an individual view for the books. We'll do that shortly. The other issue you might have noticed is that we added the classes
book-container
and book
but they don't seem to have applied any sort of styling. Let's fix that issue first.Open up the
layout.css
file located in the src/components
directory and add the following styles to the bottom of the page:1 .book-container { 2 display: flex; 3 flex-direction: row; 4 flex-wrap: wrap 5 } 6 7 .book { 8 width: 25%; 9 flex-grow: 1; 10 text-align: center; 11 } 12 13 .book img { 14 width: 50%; 15 }
Next, let's simplify our UI by just displaying the cover of the book. If a user wants to learn more about it, they can click into it. Update the
index.js
return to the following:1 const IndexPage = (props) => { 2 const books = props.data.allMongodbGatsbyBooks.edges; 3 4 return ( 5 <Layout> 6 7 <div className="book-container"> 8 {books.map(book => 9 <div className="book"> 10 {book.node.thumbnailUrl && 11 <Link to={'/book/' + book.node.id}> 12 <img src={book.node.thumbnailUrl}/> 13 </Link> 14 } 15 </div> 16 )} 17 </div> 18 19 </Layout> 20 ) 21 }
While we're at it, let's change the name of our site in the header to Books Plus by editing the
gatsby-config.js
file. Update the siteMetadata.title
property to Books Plus.Our updated UI will look like this:
As mentioned earlier, if you click on any of the book covers you will be taken to a 404 page. GatsbyJS gives us multiple ways to tackle how we want to create this page. We can get this content dynamically, but I think pre-rendering all of this pages at build time will give our users a much better experience, so we'll do that.
The first thing we'll need to do is create the UI for what our single book view page is going to look like. Create a new file in the components directory and call it
book.js
. The code for this file will look like this:1 import React from "react" 2 import { graphql } from "gatsby" 3 import Layout from "./layout" 4 5 class Item extends React.Component { 6 render() { 7 const book = this.props.data.mongodbGatsbyBooks 8 9 return ( 10 <Layout> 11 <div> 12 <img src={book.thumbnailUrl} /> 13 <h1>{book.title}</h1> 14 <p>By {book.authors.map(author => ( <span>{author}, </span>))}</p> 15 <p>{book.longDescription}</p> 16 <p>Published: {book.publishedDate} | ISBN: {book.isbn}</p> 17 {book.categories.map(category => category)} 18 </div> 19 </Layout> 20 ) 21 } 22 } 23 24 export default Item 25 26 export const pageQuery = graphql` 27 query($id: String!) { 28 mongodbGatsbyBooks(id: { eq: $id }) { 29 id 30 title 31 longDescription 32 thumbnailUrl 33 isbn 34 pageCount 35 publishedDate(formatString: "MMMM DD, YYYY") 36 authors 37 categories 38 } 39 } 40 `
To break down what is going on in this component, we are making use of the
mongodbGatsbyBooks
query which returns information requested on a single book based on the id
provided. That'll do it for our component implementation. Now let's get to the fun part.Essentially what we want to happen when we start up our Gatsby server is to go and get all the book information from our MongoDB database and create a local page for each document. To do this, let's open up the
gatsby-node.js
file. Add the following code and I'll explain it below:1 const path = require('path') 2 3 exports.createPages = async ({ graphql, actions }) => { 4 const { createPage } = actions 5 6 const { data } = await graphql(` 7 { 8 books: allMongodbGatsbyBooks { 9 edges { 10 node { 11 id 12 } 13 } 14 } 15 } 16 `) 17 18 const pageTemplate = path.resolve('./src/components/book.js') 19 20 for (const { node } of data.books.edges) { 21 createPage({ 22 path: '/book/${node.id}/', 23 component: pageTemplate, 24 context: { 25 id: node.id, 26 }, 27 }) 28 } 29 }
The above code will do the heavy lifting of going through our list of 400+ books and creating a static page for each one. It does this by utilizing the Gatsby
createPages
API. We supply the pages we want, alongside the React component to use, as well as the path and context for each, and GatsbyJS does the rest. Let's save this file, run gatsby clean
and gatsby develop
, and navigate to localhost:8000
.Now when the page loads, you should be able to click on any of the books and instead of seeing a 404, you'll see the details of the book rendered at the
/book/{id}
url.So far so good!
We've shown how we can use MongoDB as a data source for our books. The next step will be to allow us to write reviews on these books and to accomplish that we'll use a different data source: trusty old Markdown.
In the
src
directory, create a new directory and call it content
. In this directory, let's create our first post called welcome.md
. Open up the new welcome.md
file and paste the following markdown:1 --- 2 title: Welcome to Books Plus 3 author: Ado Kukic 4 slug: welcome 5 --- 6 7 Welcome to BooksPlus, your trusted source of tech book reviews!
Save this file. To use Markdown files as our source of content, we'll have to add another plugin. This plugin will be used to transform our
.md
files into digestible content for our GraphQL API as well as ultimately our frontend. This plugin is called gatsby-transformer-remark
and you can install it by running npm install --save gatsby-transformer-remark
.We'll have to configure this plugin in our
gatsby-config.js
file. Open it up and make the following changes:1 { 2 resolve: 'gatsby-source-filesystem', 3 options: { 4 name: 'content', 5 path: `${__dirname}/src/content/`, 6 }, 7 }, 8 'gatsby-transformer-remark',
The
gatsby-source-filesystem
plugin is already installed, and we'll overwrite it to just focus on our markdown files. Below it we'll add our new plugin to transform our Markdown into a format our GraphQL API can work with. While we're at it we can also remove the image.js
and seo.js
starter components as we will not be using them in our application.Let's restart our Gatsby server and navigate to the GraphiQL UI. We'll see two new queries added:
allMarkdownRemark
and markdownRemark
. These queries will allow us to query our markdown content. Let's execute the following query:1 query MyQuery { 2 allMarkdownRemark { 3 edges { 4 node { 5 frontmatter { 6 title 7 author 8 } 9 html 10 } 11 } 12 } 13 }
Our result should look something like the screenshot below, and will
look exactly like the markdown file we created earlier.
Now that we can query our markdown content, we can just as pre-generate
the markdown pages for our blog. Let's do that next. The first thing
we'll need is a template for our blog. To create it, create a new file
called
blog.js
located in the src/components
directory. My code will
look like this:1 import React from "react" 2 import { graphql } from "gatsby" 3 import Layout from "./layout" 4 5 class Blog extends React.Component { 6 render() { 7 const post = this.props.data.markdownRemark 8 9 return ( 10 <Layout> 11 <div> 12 <h1>{post.frontmatter.title}</h1> 13 <h2>By {post.frontmatter.author}</h2> 14 <div 15 className="blog-post-content" 16 dangerouslySetInnerHTML={{ __html: post.html }} 17 /> 18 </div> 19 </Layout> 20 ) 21 } 22 } 23 24 export default Blog 25 26 export const pageQuery = graphql` 27 query($id: String!) { 28 markdownRemark(frontmatter : {slug: { eq: $id }}) { 29 frontmatter { 30 title 31 author 32 } 33 html 34 } 35 } 36 `
Next we'll need to tell Gatsby to build our markdown pages at build
time. We'll open the
gatsby-node.js
file and make the following
changes:1 const path = require('path') 2 3 exports.createPages = async ({ graphql, actions }) => { 4 const { createPage } = actions 5 6 const { data } = await graphql(` 7 { 8 books: allMongodbGatsbyBooks { 9 edges { 10 node { 11 id 12 } 13 } 14 } 15 posts: allMarkdownRemark { 16 edges { 17 node { 18 frontmatter { 19 slug 20 } 21 } 22 } 23 } 24 } 25 `) 26 27 const blogTemplate = path.resolve('./src/components/blog.js') 28 const pageTemplate = path.resolve('./src/components/book.js') 29 30 for (const { node } of data.posts.edges) { 31 32 createPage({ 33 path: `/blog/${node.frontmatter.slug}/`, 34 component: blogTemplate, 35 context: { 36 id: node.frontmatter.slug 37 }, 38 }) 39 } 40 41 for (const { node } of data.books.edges) { 42 createPage({ 43 path: `/book/${node.id}/`, 44 component: pageTemplate, 45 context: { 46 id: node.id, 47 }, 48 }) 49 } 50 }
The changes we made above will not only generate a different page for
each book, but will now generate a unique page for every markdown file.
Instead of using a randomly generate id for the content page, we'll use
the user-defined slug in the frontmatter.
Let's restart our Gatsby server and navigate to
localhost:8000/blog/welcome
to see our changes in action.We want our users to be able to read our content and reviews. Currently
you can navigate to
/blog/welcome
to see the post, but it'd be nice to
display our latest blog posts on the homepage as well. To do this we'll,
make a couple of updates on our index.js
file. We'll make the
following changes:1 import React from "react" 2 import { Link } from "gatsby" 3 import { graphql } from "gatsby" 4 5 import Layout from "../components/layout" 6 7 const IndexPage = (props) => { 8 const books = props.data.books.edges; 9 const posts = props.data.posts.edges; 10 11 return ( 12 <Layout> 13 <div className="posts"> 14 {posts.map(post => 15 <div className="post"> 16 <h2><Link to={"/blog/" + post.node.frontmatter.slug}>{post.node.frontmatter.title}</Link></h2> 17 <p>By {post.node.frontmatter.author}</p> 18 </div>)} 19 </div> 20 <div className="book-container"> 21 {books.map(book => 22 <div className="book"> 23 {book.node.thumbnailUrl && 24 <Link to={'/book/' + book.node.id}> 25 <img src={book.node.thumbnailUrl}/> 26 </Link> 27 } 28 </div> 29 )} 30 </div> 31 32 </Layout> 33 ) 34 } 35 36 export default IndexPage 37 38 export const pageQuery = graphql` 39 query { 40 posts: allMarkdownRemark { 41 edges { 42 node { 43 frontmatter { 44 title 45 slug 46 author 47 } 48 } 49 } 50 } 51 books: allMongodbGatsbyBooks { 52 edges { 53 node { 54 id 55 title 56 shortDescription 57 thumbnailUrl 58 } 59 } 60 } 61 } 62 `
We've updated our GraphQL query to get us not only the list of books,
but also all of our posts. We named these queries
books
and posts
accordingly so that it's easier to work with them in our template.
Finally we updated the template to render the new UI. If you navigate to
localhost:8000
now you should see your latest post at the top like
this:And of course, you can click it to view the single blog post.
The final thing I would like to do in our blog today is the ability to
reference a book from MongoDB in our review. This way when a user reads
a review, they can easily click through and see the book information.
To get started with this, we'll need to update our
gatsby-node.js
file
to allow us to query a specific book provided in the frontmatter of a
post. We'll update the allMarkdownRemark
so that in addition to
getting the slug, we'll get the book parameter. The query will look like
this:1 allMarkdownRemark { 2 edges { 3 node { 4 frontmatter { 5 slug 6 book 7 } 8 } 9 } 10 } 11 ... 12 }
Additionally, we'll need to update our
createPage()
method when
generating the blog pages, to pass along the book information in the
context.1 createPage({ 2 path: `/blog/${node.frontmatter.slug}/`, 3 component: blogTemplate, 4 context: { 5 id: node.frontmatter.slug, 6 book: node.frontmatter.book 7 }, 8 }) 9 }
We'll be able to use anything passed in this
context
property in our
GraphQL queries in our blog component.Next, we'll update our blog component to account for the new query. This
query will be the MongoDB based book query. It will look like so:
1 export const pageQuery = graphql` 2 query($id: String!, $book: String) { 3 post: markdownRemark(frontmatter : {slug: { eq: $id }}) { 4 id 5 frontmatter { 6 title 7 author 8 } 9 html 10 } 11 book: mongodbGatsbyBooks(id: { eq: $book }) { 12 id 13 thumbnailUrl 14 } 15 } 16 `
Notice that the
$book
parameter is optional. This means that a post
could be associated with a specific book, but it doesn't have to be.
We'll update our UI to display the book information if a book is
provided.1 class Blog extends React.Component { 2 render() { 3 const post = this.props.data.post 4 const book = this.props.data.book 5 6 return ( 7 <Layout> 8 <div> 9 <h1>{post.frontmatter.title}</h1> 10 <h2>By {post.frontmatter.author}</h2> 11 <div 12 className="blog-post-content" 13 dangerouslySetInnerHTML={{ __html: post.html }} 14 /> 15 16 {book && 17 <Link to={"/book/" + book.id}> 18 <img src={book.thumbnailUrl} /> 19 </Link> 20 } 21 </div> 22 </Layout> 23 ) 24 } 25 }
If we look at our original post, it doesn't have a book associated with
it, so that specific post shouldn't look any different. But let's write
a new piece of content, that does contain a review of a specific book.
Create a new markdown file called
mongodb-in-action-review.md
. We'll
add the following review:1 --- 2 title: MongoDB In Action Book Review 3 author: Ado Kukic 4 slug: mdb-in-action-review 5 book: 30e4050a-da76-5c08-a52c-725b4410e69b 6 --- 7 8 MongoDB in Action is an essential read for anybody wishing to learn the ins and outs of MongoDB. Although the book has been out for quite some time, it still has a lot of valuable information and is a great start to learning MongoDB.
Restart your Gatsby server so that the new content can be generated. On
your homepage, you'll now see two blog posts, the original Welcome
post as well as the new MongoDB In Action Review post.
Clicking the MongoDB In Action Review link will take you to a blog
page that contains the review we wrote a few seconds ago. But now,
you'll also see the thumbnail of the book. Clicking this thumbnail will
lead you to the books page where you can learn more about the book.
In this tutorial, I showed you how to build a modern blog with GatsbyJS.
We used multiple data sources, including a remote MongoDB
Atlas database and local markdown
files, to generate a static blog. We took a brief tour of GraphQL and
how it enhances our development experience by consolidating all of our
data sources into a single API that we can query both at build and run
time. I hope you learned something new, if you have any questions feel
free to ask in our MongoDB community
forums.
If you want to get the code for this tutorial, you can clone it from this GitHub repo. The sample books dataset can also be found here. Try MongoDB Atlas to make it easy to manage and scale your MongoDB database.
Happy coding!