Add realtime comments to a Gatsby blog
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:
Realtime comments demo
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:
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. Anid
is added to the comment data to identify it as well as a timestamp. Thedata
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:
Here’s what we want our blog to look like:
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:
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;
}
Link component
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 theappKey
that can be found in the Pusher dashboard. Be sure to replace the placeholder string with your realappKey
. -
In the
componentDidMount
lifecycle, we subscribed to thepost-comment
channel and listened for anew-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 thesrc/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.
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.
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.
18 June 2018
by Christian Nwamba