Building a Jekyll Site – Part 3 of 3: Creating a Firebase-Backed Commenting System

The following is a guest post by Mike Neumegen from CloudCannon. This final post is about adding some functionality to a Jekyll site that isn't possible: comments. That's because Jekyll has no backend component in which to save comments. But, we don't even need that if we do it entirely front-end with Firebase!

Article Series:

  1. Converting a Static Website To Jekyll
  2. Adding a Jekyll CMS with CloudCannon
  3. Creating a Firebase-Backed Commenting System (You are here!)

In this series, we're building a site with a blog and content management system for Coffee Cafe, a fictional cafe. This final post is about building a custom commenting system with Firebase.

Custom built solutions provide more control of the design, functionality and data than drop-in solutions, such as Disqus and Facebook Comments.

What is Firebase?

Firebase is a real-time, scalable backend. It allows developers to build applications with authentication and persistent data for static websites.

We're going to store our blog comments in Firebase and retrieve them when someone views a blog post.

Sign Up

First, sign up for a Firebase account.

Once you've signed up, create a new app for the blog comments and record the App URL for later.

Setup

We need a number of JavaScript libraries to run the commenting system. Firebase saves and fetches comments, jQuery adds elements to the page, Moment formats dates, and blueimp-md5 generates MD5s. `/js/blog.js` contains the custom application code for the commenting system

Add the following scripts above </body> in `_layouts/default.html` (or do whatever build process / concatenation thing you normally do):

<script src="https://cdn.firebase.com/js/client/2.2.1/firebase.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.11.0/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/blueimp-md5/2.1.0/js/md5.js"></script>
<script src="/js/blog.js"></script>

Firebase Overview

When a visitor views a blog post we get all the relevant comments from Firebase.

Visitors post comments with a name, email address and message. We take this information, add a timestamp and the current page, then store it in Firebase.

In Firebase, data is stored as JSON objects. The comments are stored as an array of objects for each blog post:

{
  "/tutorial/2016/01/02/title-tag.html": [
    {
      "name": "Bill",
      "email": "bill@example.org",
      "message": "Hi there, nice blog!",
      "timestamp": 1452042357209
    },
    {
      "name": "Bob",
      "email": "bob@example.org",
      "message": "Wow look at this blog.",
      "timestamp": 145204235846
    }
  ],
  "/announcement/2016/01/01/latest-seo-trends.html": [
    {
      "name": "Steve",
      "email": "steve@example.org",
      "message": "First post!",
      "timestamp": 1452043267245
    }
  ]
}

Implementation

Firebase references provide read and write access to the database. Add a reference to the database in `/js/blog.js`:

var ref = new Firebase("https://<YOUR-APP-ID>.firebaseio.com/");

ref gives us access to the root of the database. We can get a reference to a blog post using ref.child("<PATH_TO_BLOG_POST>").

Saving Comments

The path is a great way to identify a blog post, but Firebase doesn't support characters like ampersands in the key name. To solve this issue, add a function to replace unsupported characters:

function slugify(text) {
  return text.toString().toLowerCase().trim()
    .replace(/&/g, '-and-')
    .replace(/[\s\W-]+/g, '-')
    .replace(/[^a-zA-Z0-9-_]+/g,'');
}

Save a reference to the slugified current path:

var postRef = ref.child(slugify(window.location.pathname));

Add a form to post new comments below the blog posts. Enter the following markup below {{ content }} in `_layouts/post.html`:

<h3>Leave a comment</h3>

<form id="comment">
  <label for="message">Message</label>
  <textarea id="message"></textarea>

  <label for="name">Name</label>
  <input type="text" id="name">

  <label for="email">Email</label>
  <input type="text" id="email">

  <input type="submit" value="Post Comment">
</form>

To send the data to Firebase when the form is submitted, override the default submit listener in `/js/blog.js`:

$("#comment").submit(function() {
  postRef.push().set({
    name: $("#name").val(),
    message: $("#message").val(),
    md5Email: md5($("#email").val()),
    postedAt: Firebase.ServerValue.TIMESTAMP
  });

  $("input[type=text], textarea").val("");
  return false;
});

postRef.push() creates an array in Firebase if it doesn't exist and returns a reference to the first item. set saves the data to Firebase.

We store an MD5 of the email address to protect the privacy of commenters since the data is public. Gravatar uses MD5s to display profile images.

Instead of new Date().getTime() for the timestamp, we use Firebase.ServerValue.TIMESTAMP. This is a timestamp from Firebase servers which avoids timezone issues and forged requests.

Displaying Comments

Add a container to hold comments the above the comment form in _layouts/post.html:

<hr>

<div class="comments"></div>

Firebase has a reference to listen for new comments. The child_added event triggers for existing and new comments. We use the same event to render all comments.

child_added returns a current snapshot of the data. We get the data from the snapshot, format it into HTML then prepend it to <div class="comments"></div>.

postRef.on("child_added", function(snapshot) {
      var newPost = snapshot.val();
      $(".comments").prepend('<div class="comment">' +
        '<h4>' + escapeHtml(newPost.name) + '</h4>' +
        '<div class="profile-image"><img src="http://www.gravatar.com/avatar/' + escapeHtml(newPost.md5Email) + '?s=100&d=retro"/></div> ' +
        '' + moment(newPost.postedAt).fromNow() + '<p>' + escapeHtml(newPost.message)  + '</p></div>');
    });

The Complete File

Save the complete file to `/js/blog.js`. Change <YOUR-APP-ID> to ID you recorded earlier.

$(function() {
  var ref = new Firebase("https://comment-jekyll-csstricks.firebaseio.com/"),
    postRef = ref.child(slugify(window.location.pathname));

    postRef.on("child_added", function(snapshot) {
      var newPost = snapshot.val();
      $(".comments").prepend('<div class="comment">' +
        '<h4>' + escapeHtml(newPost.name) + '</h4>' +
        '<div class="profile-image"><img src="http://www.gravatar.com/avatar/' + escapeHtml(newPost.md5Email) + '?s=100&d=retro"/></div> ' +
        '<span class="date">' + moment(newPost.postedAt).fromNow() + '</span><p>' + escapeHtml(newPost.message)  + '</p></div>');
    });

    $("#comment").submit(function() {
      var a = postRef.push();
      
      a.set({
        name: $("#name").val(),
        message: $("#message").val(),
        md5Email: md5($("#email").val()),
        postedAt: Firebase.ServerValue.TIMESTAMP
      });

      $("input[type=text], textarea").val("");
      return false;
    });
});

function slugify(text) {
  return text.toString().toLowerCase().trim()
    .replace(/&/g, '-and-')
    .replace(/[\s\W-]+/g, '-')
    .replace(/[^a-zA-Z0-9-_]+/g,'');
}


function escapeHtml(str) {
    var div = document.createElement('div');
    div.appendChild(document.createTextNode(str));
    return div.innerHTML;
}

The completed commenting system looks like this:

Try out a working demo here. Open two windows and post a comment, you'll see it appear in both windows straight away.

Security

At the moment, anyone can edit or delete comments. For basic security we'll make a rule that visitors can only add comments. In Firebase, open up the Security and Rules tab:

The current rules allow global reads and writes. To prevent Firebase deleting or writing data if it already exists, change .write to:

{
    "rules": {
        ".read": true,
        ".write": "false",
        "$slug": {
          ".write": "!data.exists()",
          "$message": {
            ".write": "!data.exists() && newData.exists()"
          }
        }
    }
}

A full set of authentication options is available to build something more complex.

The Finished Site

With a few libraries and 31 lines of JavaScript, we have a full featured backend for blog comments working on a static website.

That brings us to the end of this series. In three short tutorials, we've gone from a static site to an updatable, live Jekyll site with our own commenting system.

Article Series:

  1. Converting a Static Website To Jekyll
  2. Adding a Jekyll CMS with CloudCannon
  3. Creating a Firebase-Backed Commenting System (You are here!)