Back to search

Add realtime comments to a Gatsby blog

  • Christian Nwamba
June 18th, 2018
You will need Node 6+ and npm installed on your machine. Some knowledge of React and Node may be helpful.

Introduction

We all dream of not just owning a blog but actually having the time to write and keep the blog up to date. Creating a blog has been made easy by static site generators like Jekyll but today we’ll be using Gatsby. Gatsby is a blazing-fast static site generator for React.

In this tutorial, you’ll learn how to set up a blog using Gatsby. Also, we’ll add realtime comments into our blog with the help of Pusher.

Here’s a screenshot of the final product:

gatsby-blog-comments-demo-1

Realtime comments demo

gatsby-blog-comments-demo-2

Prerequisites

To follow this tutorial a basic understanding of how to use Gatsby, React and Node.js. Please ensure that you have at least Node version 6>= installed before you begin.

We’ll be using these tools to build our application:

We’ll be sending messages to the server and using Pusher’s pub/sub pattern, we’ll listen to and receive messages in realtime. To make use of Pusher you’ll have to create an account here.

After account creation, visit the dashboard. Click Create new Channels app, fill out the details, click Create my app, and make a note of the details on the App Keys tab.

Initializing the application and installing dependencies

To get started, we will use the blog starter template to initialize our application. The first step is to install the Gatsby CLI. To install the CLI, run the following command in the terminal:

    npm install -g gatsby-cli

If you use Yarn run:

    yarn global add gatsby-cli

The next step is to create our project with the help of the CLI. Run the command below to create a project called realtime-blog using the blog starter template:

    gatsby new realtime-blog https://github.com/HackAfro/gatsby-blog-starter-kit.git

Next, run the following commands in the root folder of the project to install dependencies.

    // install depencies required to build the server
    npm install express body-parser dotenv pusher uuid 

    // front-end dependencies
    npm install pusher-js

Start the app server by running npm run develop in a terminal in the root folder of your project.

A browser tab should open on http://localhost:8000. The screenshot below should be similar to what you see in your browser:

gatsby-blog-default

Building our server

We’ll build our server using Express. Express is a fast, unopinionated, minimalist web framework for Node.js.

Create a file called server.js in the root of the project and update it with the code snippet below

    // server.js

    require('dotenv').config();
    const express = require('express');
    const bodyParser = require('body-parser');
    const Pusher = require('pusher');

    const app = express();
    const port = process.env.PORT || 4000;
    const pusher = new Pusher({
      appId: process.env.PUSHER_APP_ID,
      key: process.env.PUSHER_KEY,
      secret: process.env.PUSHER_SECRET,
      cluster: process.env.PUSHER_CLUSTER,
    });

    app.use(bodyParser.json());
    app.use(bodyParser.urlencoded({extended: false}));
    app.use((req, res, next) => {
      res.header('Access-Control-Allow-Origin', '*');
      res.header(
        'Access-Control-Allow-Headers',
        'Origin, X-Requested-With, Content-Type, Accept'
      );
      next();
    });

    app.listen(port, () => {
      console.log(`Server started on port ${port}`);
    });

The calls to our endpoint will be coming in from a different origin. Therefore, we need to make sure we include the CORS headers (Access-Control-Allow-Origin). If you are unfamiliar with the concept of CORS headers, you can find more information here.

Create a Pusher account and a new Pusher Channels app if you haven’t done so yet and get your appId, key and secret.

Create a file in the root folder of the project and name it .env. Copy the code snippet below into the .env file and ensure to replace the placeholder values with your Pusher credentials.

    // .env

    // Replace the placeholder values with your actual pusher credentials
    PUSHER_APP_ID=PUSHER_APP_ID
    PUSHER_KEY=PUSHER_KEY
    PUSHER_SECRET=PUSHER_SECRET
    PUSHER_CLUSTER=PUSHER_CLUSTER

We’ll make use of the dotenv library to load the variables contained in the .env file into the Node environment. The dotenv library should be initialized as early as possible in the application.

Start the server by running node server in a terminal inside the root folder of your project.

Draw route

Let’s create a post route named comment, the Gatsby application will send requests to this route containing the comment data needed to update the application.

    // server.js
    require('dotenv').config();
    ...
    const { v4 } = require('uuid');

    app.use((req, res, next) => {
      res.header('Access-Control-Allow-Origin', '*');
      ...
    });


    app.post('/comment', (req, res) => {
      const {body} = req;
      const data = {
        ...body,
        timestamp: new Date(),
        id: v4(),
      };
      pusher.trigger('post-comment', 'new-comment', data);
      res.json(data);
    });

     ...
  • The request body will be sent as the data for the triggered Pusher event. An object data is created containing the request body. An id is added to the comment data to identify it as well as a timestamp. The data object will be sent as a response to the user.
  • The trigger is achieved using the trigger method which takes the trigger identifier(post-comment), an event name (new-comment), and a payload(data).

Building our blog index page

The current look of our blog is too generic, we’d like to have our blog represent our budding personality. To get that look, we’ll change the layout of the blog and add a few CSS styles to update the look and feel of the blog.

Here’s the current look of our blog index page:

gatsby-blog-default

Here’s what we want our blog to look like:

gatsby-blog-comments-demo-1

I hope this new look will represent your budding personality because it really represents mine. Let’s go through the steps we’ll take to achieve this new look.

Open the index.js file in the src/pages/ directory. Update the file to look like the snippet below:

    // src/pages/index.js

    import React from 'react';
    import GatsbyLink from 'gatsby-link';
    import Link from '../components/Link';
    import Tags from '../components/Tags';
    import '../css/index.css';

    export default function Index({ data }) {
      const { edges: posts } = data.allMarkdownRemark;
      return (
        <div className="blog-posts">
          {posts
            .filter((post) => post.node.frontmatter.title.length > 0)
            .map(({ node: post }, index) => {
              return (
                <div
                  className={`blog-post-preview ${
                    index % 2 !== 0 ? 'inverse' : ''
                  }`}
                  key={post.id}
                >
                  <div className="post-info">
                    <h1 className="title">
                      <GatsbyLink to={post.frontmatter.path}>
                        {post.frontmatter.title}
                      </GatsbyLink>
                    </h1>
                    <div className="meta">
                      <div className="tags">
                        <Tags list={post.frontmatter.tags} />
                      </div>
                      <h4 className="date">{post.frontmatter.date}</h4>
                    </div>
                    <p className="excerpt">{post.excerpt}</p>
                    <div>
                      <Link to={post.frontmatter.path} className="see-more">
                        Read more
                      </Link>
                    </div>
                  </div>
                  <div className="post-img">
                    <img src={post.frontmatter.image} alt="image" />
                  </div>
                </div>
              );
            })}
        </div>
      );
    }
    export const pageQuery = graphql`
      query IndexQuery {
        allMarkdownRemark(sort: { order: DESC, fields: [frontmatter___date] }) {
          edges {
            node {
              excerpt(pruneLength: 250)
              id
              frontmatter {
                title
                date(formatString: "MMMM DD, YYYY")
                path
                tags
                image
              }
            }
          }
        }
      }
    `;

There’s really not much going on here. First, made the blog content separate from the blog image. Then we checked if the index of the current post was an odd number, if true, we added an inverse class to the post.

Since we’ll be using flex for the layout, if we make the flex-direction: row-inverse it will invert the layout making the image appear on the left side rather than the right. Finally, we included an image for each blog post even though the posts don’t have an image front matter variable.

After this update you’ll get an error in your terminal similar to the screenshot below:

gatsby-blog-comments-error

This is because the image variable doesn’t exist on the markdown files that we currently have. We’ll get to updating the markdown files so ignore the error for now.

Next step is to update the stylesheet associated with the index page. Open the index.css file in the /src/css directory and update it like so:

    // /src/css/index.css

    .blog-post-preview {
      display: flex;
      align-items: flex-start;
      justify-content: center;
      padding: 1rem 0.25rem;
      border-bottom: 2px solid rgba(0, 0, 0, 0.04);
      margin-bottom: 20px;
    }
    .blog-post-preview.inverse{
      flex-direction: row-reverse;
    }
    .blog-post-preview:last-child {
      border-bottom-width: 0;
    }
    .post-info {
      flex: 1;
    }
    .blog-post-preview.inverse > .post-img{
      margin-left: 0;
      margin-right: 1rem;
    }
    .post-img {
      flex: 1;
      margin-left: 1rem;
    }
    .post-img > img {
      max-width: 100%;
      max-height: 100%;
    }
    .title {
      font-size: 22px;
      text-transform: uppercase;
      margin-bottom: 2px;
      line-height: 1.2;
    }
    .title > a {
      color: black;
      text-decoration: none;
      opacity: 0.7;
      letter-spacing: -0.2px;
    }
    .date {
      font-size: 13px;
      opacity: 0.5;
      margin: 0;
    }
    .meta {
      display: flex;
      align-items: center;
      margin-bottom: 8px;
    }
    .excerpt {
      font-size: 15px;
      opacity: 0.7;
      letter-spacing: 0.4px;
      margin-bottom: 10px;
    }

Next, we’ll update the components associated with the index page. Currently, we have the Link and Tags components being used on the index page. Let’s update them to match the current flow of our application.

Tags component

Open the Tags.js file in the /src/components directory and update it with the content below:

    // /src/components/Tags.js

    import React from 'react';
    import Link from 'gatsby-link';
    import TagIcon from 'react-icons/lib/fa/tag';

    import '../css/tags.css';

    export default function Tags({ list = [] }) {
      return (
        <ul className="tags">
          {list.map(tag =>
            <li key={tag}>
              <Link to={`/tags/${tag}`} className="tag">
                <TagIcon size={15} className="icon white" />
                {tag}
              </Link>
            </li>
          )}
        </ul>
      );
    }

To update the stylesheet associated with it, open the tags.css file in the src/css/ directory. Copy the contents below into it:

    // /src/css/tags.css

    .tags {
      display: flex;
      margin-right: 6px;
      list-style: none;
      padding: 0;
      margin: 0 4px 0 0;
    }
    .tag {
      color: white;
      background: purple;
      font-size: 11px;
      text-transform: uppercase;
      font-weight: bold;
      margin: 3px;
      border-radius: 35px;
      padding: 5px 12px;
      line-height: 12px;
      font-family: 'Rajdhani', cursive;
      text-decoration: none;
    }

This component will build ontop the GatsbyLink component provided by Gatsby. It’ll add a custom class to the GatsbyLink component. The Link.js file will stay the same. We’ll only be updating the stylesheet associated with this component. Open the link.css file in the src/css folder and update it by adding the following styles to it:

    .link {
      color: black;
      opacity: 0.6;
      background: white;
      box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.07);
      text-decoration: none;
      padding: 7px 15px;
      border-radius: 34px;
      font-size: 12px;
      text-transform: uppercase;
      font-weight: bold;
      border: 1px solid rgba(0, 0, 0, 0.05);
    }

Finally, we’ll update the blog header. The header can be found in the index.js file in the src/layouts directory. Open it and replace the contents with the code below:

    // src/layouts/index.js

    import React from 'react';
    import PropTypes from 'prop-types';
    import Link from 'gatsby-link';
    import Helmet from 'react-helmet';
    import '../css/typography.css';
    import '../css/layout.css';

    export default class Template extends React.Component {
      static propTypes = {
        children: PropTypes.func,
      };
      render() {
        const { location } = this.props;
        const isRoot = location.pathname === '/';
        return (
          <div>
            <Helmet
              title="Gatsby Default (Blog) Starter"
              meta={[
                { name: 'description', content: 'Sample' },
                { name: 'keywords', content: 'sample, something' },
              ]}
            />
            <div
              style={{
                background: `white`,
                marginBottom: `1.45rem`,
                boxShadow: '0 2px 4px 0 rgba(0,0,0,0.1)',
              }}
            >
              <div
                style={{
                  margin: `0 auto`,
                  maxWidth: 960,
                  padding: isRoot ? `0.7rem 1.0875rem` : `.5rem 0.75rem`,
                }}
              >
                <h1 style={{ margin: 0, fontSize: isRoot ? `2rem` : `1.5rem` }}>
                  <Link
                    to="/"
                    style={{
                      color: 'purple',
                      textDecoration: 'none',
                      fontFamily: "'Lobster', sans-serif",
                    }}
                  >
                    The Food Blog
                  </Link>
                </h1>
              </div>
            </div>
            <div
              style={{
                margin: `0 auto`,
                maxWidth: 960,
                padding: `0px 1.0875rem 1.45rem`,
                paddingTop: 0,
              }}
            >
              {this.props.children()}
            </div>
          </div>
        );
      }
    }

In the snippet above, we added a stylesheet layout.css and updated the inline styles in the component. Let’s create the layout.css in the src/css/ directory. Open the file and copy the code snippet below into it:

    // layout.css

    @import url('https://fonts.googleapis.com/css?family=Lobster|Rajdhani:600|Source+Sans+Pro:400,600,700');
    * {
      font-family: 'Source Sans Pro', sans-serif;
    }
    body {
      background: rgba(0, 0, 0, 0.06);
    }
    .icon {
      color: purple;
      margin: 0 3px;
    }
    .icon.white {
      color: white;
    }

Now our index page should look like the screenshot of the potential index page we saw above. Now that’s progress.

Adding and updating blog posts

So far we’ve updated the look and layout of our blog. Let’s add a new blog post just to see how our index page handles it. Also, we’ll update the markdown files to include an image variable in the front matter section.

Update all the current posts to have the same structure as the content below:

    ---
    path: "/post-new.html"
    date: "2018-06-10T13:56:24.754Z"
    title: "A post by me"
    tags: ["new", "creative"]
    image: "https://source.unsplash.com/random/1000x500"
    ---

    Post content ...

We’ll be including random images from Unsplash for our blog images. Update all the markdown files to include an image variable. Then restart the server or you’ll end up like me debugging the application for ten minutes trying to figure out the error. The error on the terminal should be cleared once you updated the markdown files and restart the server.

Updating the blog detail page

Now that our index page reflects our personality, let’s do the same with the blog details page. Open the blog-post.js file in the src/templates directory and update it to look like the snippet below:

    // src/templates/blog-post.js

    import React from 'react';
    import Helmet from 'react-helmet';
    import BackIcon from 'react-icons/lib/fa/chevron-left';
    import ForwardIcon from 'react-icons/lib/fa/chevron-right';
    import Link from '../components/Link';
    import Tags from '../components/Tags';
    import '../css/blog-post.css';

    export default function Template({ data, pathContext }) {
      const { markdownRemark: post } = data;
      const { next, prev } = pathContext;
      return (
        <div className="blog-post-container">
          <Helmet title={`The Food Blog - ${post.frontmatter.title}`} />
          <div className="blog-post">
            <div>
              <h1 className="title">{post.frontmatter.title}</h1>
              <h2 className="date">{post.frontmatter.date}</h2>
              <div className="post-body">
                <div className="post-img">
                  <img src={post.frontmatter.image} alt="" />
                </div>
                <div
                  className="blog-post-content post-info"
                  dangerouslySetInnerHTML={{ __html: post.html }}
                />
              </div>
              <Tags list={post.frontmatter.tags || []} />
              <div className="navigation">
                {prev && (
                  <Link className="link prev" to={prev.frontmatter.path}>
                    <BackIcon size={16} className="icon" /> {prev.frontmatter.title}
                  </Link>
                )}
                {next && (
                  <Link className="link next" to={next.frontmatter.path}>
                    {next.frontmatter.title}{' '}
                    <ForwardIcon size={16} className="icon" />
                  </Link>
                )}
              </div>
              <div className="comment-section">
                <h4 className="comment-header">Comments</h4>
                {/* Comment component comes here */}
              </div>
            </div>
          </div>
        </div>
      );
    }
    export const pageQuery = graphql`
      query BlogPostByPath($path: String!) {
        markdownRemark(frontmatter: { path: { eq: $path } }) {
          html
          frontmatter {
            date(formatString: "MMMM DD, YYYY")
            path
            tags
            title
            image
          }
        }
      }
    `;

Let’s update the stylesheet associated with it. Open the blog-post.css file in the src/css directory. Make the content similar to the snippet below:

    // src/css/blog-post.css

    .blog-post .link.prev {
      float: left;
    }
    .blog-post .link.next {
      float: right;
    }
    .blog-post .title,
    .blog-post .date {
      text-align: center;
      margin: 0;
      padding: 0;
    }
    .blog-post .date {
      color: #555;
      margin-bottom: 1rem;
    }
    .blog-post .navigation {
      min-height: 60px;
      margin-top: 15px;
    }
    .blog-post-content {
      font-size: 15px;
      opacity: 0.8;
    }
    .post-info {
      flex: 2;
    }
    .post-img {
      margin-right: 1.3rem;
      padding: 2% 2% 1%;
    }
    .post-img > img {
      box-shadow: 0 3px 5px 1px rgba(0, 0, 0, 0.3);
    }
    .comment-section{
      margin-top: 30px;
    }
    .comment-header {
      font-size: 16px;
      text-transform: uppercase;
      color: purple;
      letter-spacing: -0.3px;
      margin-bottom: 10px;
    }

Realtime comments using Pusher

We’ve created a working blog and then updated the layout and styles to suit our needs yet we still don’t have a comments section for our readers to leave their thought on a blog post. We want our comment section to have some realtime functionalities where users get updates on the post as it happens. Using Pusher’s pub/sub functionality we can achieve this.

We already have Pusher dispatching events on the server, the next step is creating a listener to act on the events.

Create a folder called comments in the components folder. Create a file called form.js in the comments folder. Update the contents of the file with the snippet below:

    // src/components/comments/form.js

    import React from 'react';
    class CommentForm extends React.Component {
      constructor() {
        super();
        this.state = {
          name: '',
          comment: '',
        };
        this.handleSubmit = this.handleSubmit.bind(this);
        this.handleChange = this.handleChange.bind(this);
      }
      async handleSubmit(e) {
        e.preventDefault();
        const body = JSON.stringify({ ...this.state });
        const response = await fetch('http://localhost:4000/comment', {
          method: 'post',
          body,
          headers: {
            'content-type': 'application/json',
          },
        });
        const data = await response.json();
        this.setState({ comment: '', name: '' });
      }
      handleChange({ target }) {
        const { name, value } = target;
        this.setState({ [name]: value });
      }
      render() {
        const { name, comment } = this.state;
        return (
          <form onSubmit={this.handleSubmit} className="comment-form">
            <input
              placeholder="Your Name"
              value={name}
              name="name"
              onChange={this.handleChange}
            />
            <textarea
              placeholder="Enter your comment"
              rows="4"
              name="comment"
              value={comment}
              onChange={this.handleChange}
            />
            <div>
              <button className="button submit-button">Submit</button>
            </div>
          </form>
        );
      }
    }
    export default CommentForm;

The form component will handle the commenting functionality for users. We’ll place the form component in the CommentList component. The CommentList component hasn’t been created yet, we’ll get to that.

The next step is to create a Comment.js file. This component will display a comment from the list of comments. Update the contents of the file with the snippet below:

    // src/components/comments/Comment.js

    import React from 'react';
    const Comment = ({ comment }) => (
      <div className="comment">
        <div className="comment__meta">
          <h5>{comment.name}</h5>
          <span>{new Date(comment.timestamp).toDateString()}</span>
        </div>
        <p className="comment__body">{comment.comment}</p>
      </div>
    );
    export default Comment;

The final step is to create a file called CommentList.js in the comments folder. The component will the hold the form and Comment components. Open the file and update it with the code below:

    // src/components/comments/CommentList.js

    import React from 'react';
    import Pusher from 'pusher-js';
    import CommentForm from './form';
    import Comment from './Comment';
    import '../../css/comment.css';

    class Comments extends React.Component {
      constructor() {
        super();
        this.state = {
          comments: [],
        };
        this.pusher = new Pusher('PUSHER_KEY', {
          cluster: 'eu',
        });
      }
      componentDidMount() {
        const channel = this.pusher.subscribe('post-comment');
        channel.bind('new-comment', (data) => {
          this.setState((prevState) => ({
            comments: [...prevState.comments, data],
          }));
        });
      }

      render() {
        const { comments } = this.state;
        return (
          <div>
            <CommentForm />
            <hr />
            <div className="comment-list">
              {comments.length ? (
                comments.map((comment) => (
                  <Comment comment={comment} key={comment.id} />
                ))
              ) : (
                <h5 className="no-comments-alert">
                  No comments on this post yet. Be the first
                </h5>
              )}
            </div>
          </div>
        );
      }
    }
    export default Comments;

There’s quite a bit going on in here. We’ll walk through it.

  • In the component’s constructor, we initialized the Pusher library using the appKey that can be found in the Pusher dashboard. Be sure to replace the placeholder string with your real appKey.
  • In the componentDidMount lifecycle, we subscribed to the post-comment channel and listened for a new-comment event. In the event callback, we appended the data returned to the list of comments.
  • Also, we included a new stylesheet that hasn’t been created yet. Create a file called comment.css in the src/css directory.

Open the file and update it with the content below:

    // src/css/comment.css

    .comment-form {
      display: flex;
      flex-direction: column;
      width: 50%;
      padding: 10px 25px 20px 0;
    }
    .comment-form > input,
    .comment-form > textarea {
      width: 100%;
      border: 3px solid rgb(143, 51, 143);
      margin: 12px 0;
      padding: 7px 14px;
      font-size: 14px;
      opacity: 0.8;
      font-weight: bold;
      box-shadow: 0 2px 3px 0 rgba(0, 0, 0, 0.3);
      border-radius: 8px;
    }
    .comment-form > div > .submit-button {
      padding: 8px 45px;
      background: rgb(143, 51, 143);
      color: whitesmoke;
      border-radius: 35px;
      box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.3);
      text-transform: uppercase;
      font-size: 16px;
      font-weight: bold;
      cursor: pointer;
    }
    .comment__meta > h5 {
      font-size: 15px;
      color: purple;
      opacity: 0.7;
      margin-bottom: 3px;
      line-height: 1;
    }
    hr {
      background: rgba(0, 0, 0, 0.2);
      height: 3px;
    }
    .comment__meta > span {
      font-size: 14px;
      font-weight: bold;
      opacity: 0.5;
    }
    .comment__body {
      font-size: 18px;
      opacity: 0.8;
      font-family: 'Rajdhani', cursive;
    }
    .no-comments-alert {
      font-size: 16px;
      color: purple;
      opacity: 0.7;
      text-transform: uppercase;
      letter-spacing: -0.3px;
    }

Including comments in blog posts

Let’s include the comment section we just created in the blog post template. Open the blog-post.js file and include the comments component where we had the comment comment component comes here.

    // src/templates/blog-post.js
    ...
    import '../css/blog-post.css';
    import Comments from '../components/Comments/CommentList';
    ...

    export default function Template({ data, pathContext }) {
      ...
      return (
        ...
        <div className="comment-section">
          <h4 className="comment-header">Comments</h4>
          <Comments />
        </div>
        ...
      )
    };
    ...

Let’s have a look at our blog details page. Click on the link for any blog list item. The view should be similar to the screenshot below:

P.S: Ensure you have the server and the Gatsby dev server running.

gatsby-blog-comments-single-entry

You can also test the realtime functionality of the application by opening two browsers side by side. A Comment placed on one browser window can be seen in the other.

gatsby-blog-comments-two-browser

Conclusion

We’ve created a blog using Gatsby and included realtime commenting functionality using Pusher. You could do one extra and include a way to persist comments on a blog post. You can find the source code for this tutorial on GitHub.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.