Self-host comments in Jekyll, powered by Firebase real-time database

Self-host comments in Jekyll, powered by Firebase real-time database

connect with your readers

Overview

It’s convenient to set up a comment system in Jekyll site with external social comment systems like Disqus or Duoshuo (多说). However, as you all know, Disqus was blocked in China and Duoshuo is going to shutdown. It’s the time to rethink about the comment system (although I didn’t get too many comments →_→), simple and controllable.

If you search for “Jekyll comments”, there are several plugins or solutions that can be used. Looked into those posts, Going Static: Episode II — Attack of the Comments using Staticman seemed most perfect, all the comments become static files in your git repository. Perfect Jekyll way! But, how about situations like mine, hosting site other than GitHub Pages.

Then, Creating a Firebase-Backed Commenting System attracted my attention. Using Firebase real-time database and a little bit JavaScript, it’s easy to set up your custom commenting system. Actually, I used Firebase a lot on this site, pageview counts, trending posts and a tiny “Like” button… Why not benefit more from it!

The following steps are adapted from the above post, and add the Markdown support for the comment system. And thanks to JiYou for the discussion.

Firebase Setup

Firebase is currently a part of Google Developers tool, its real-time database stores data as JSON objects. For example, we can construct the comments like this way:

{
  "/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
    }
  ]
}

Using JavaScript, we can access the JSON database by Firebase references, that’s our comment system comes.

Create new Firebase project

The first step is to create a new project in the Firebase console.

create a new Firebase project

Project settings

In the Overview pannel, click the Add Firebase to your web app to get the initialize parameters for later use:

// TODO: Replace with your project's customized code snippet
<script src="https://www.gstatic.com/firebasejs/3.4.0/firebase.js"></script>
<script>
// Initialize Firebase
var config = {
    apiKey: '<your-api-key>',
    authDomain: '<your-auth-domain>',
    databaseURL: '<your-database-url>',
    storageBucket: '<your-storage-bucket>'
};
firebase.initializeApp(config);
</script>

JavaScript implementation

Initialize Firebase

Here, I’m going to script in a separate JavaScript file like comment.js together with jQuery. For initialising:

$.getScript('https://www.gstatic.com/firebasejs/3.4.0/firebase.js', function () {
    // Initialize Firebase
    var config = {
        apiKey: '<your-api-key>',
        authDomain: '<your-auth-domain>',
        databaseURL: '<your-database-url>',
        storageBucket: '<your-storage-bucket>'
    };
    firebase.initializeApp(config);

    // TO-DO
}

The following parts of JavaScript will fill into the TO-DO above.

New reference for comments

Add a reference to the Firebase database, that we can store new comments or read exist comments.

var rootRef = firebase.database().ref();
var postComments = rootRef.child('postComments');

Here, rootRef is the root of the Firebase database and a child postComments for all the comments.

Post identity

Similar to Disqus, we need to setup an unique identity for each blog post.

var link = $("link[rel='canonical']").attr("href");
var pathkey = decodeURI(link.replace(new RegExp('\\/|\\.', 'g'),"_"));
var postRef = postComments.child(pathkey);

Here, I used the canonical link for each post. You can replace it with window.location.pathname if you prefer.

As Firebase doesn’t support certain characters for the node key, replace these characters and used it for post-identification. postRef create a unique reference for each post under the postComments reference.

If you create your own keys, they must be UTF-8 encoded, can be a maximum of 768 bytes, and cannot contain ., $, #, [, ], /, or ASCII control characters 0-31 or 127.

Save new comments

Now, look at your comment form in the Jekyll layout. If you don’t have one, just add it below the content of the post formatted in this way:

<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>

Override the default submit action in JavaScript:

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

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

  return false;
});

postRef.push() creates an array in Firebase database if it doesn’t exist and returns a new reference to the first item. It looks like this in my test project:

new comment stored in Firebase

Here, MD5 of the email address is stored and used for display profile images from Gravatar. Thus, include the JavaScript MD5 library before the comment.js file. firebase.database.ServerValue.TIMESTAMP is the timestamp from the Firebase that can avoid time zone issues.

Showing comments

Well, we can send new comments to the Firebase database now. Let’s take a further step to pull stored comments to the post page.

Before that, add a HTML container to hold the comments in the Jekyll layout:

<div id="comments-container"></div>

Then a new JavaScript function to trigger existing and new comments:

postRef.on("child_added", function(snapshot) {
    var newComment = snapshot.val();
    
    // Markdown into HTML
    var converter = new showdown.Converter();
    converter.setFlavor('github');
    var markedMessage = converter.makeHtml(newComment.message);
    
    // HTML format
    var html = "<div class='comment'>";
    html += "<h4>" + newComment.name + "</h4>";
    html += "<div class='profile-image'><img src='https://www.gravatar.com/avatar/" + newComment.md5Email + "?s=100&d=retro'/></div>";
    html += "<span class='date'>" + jQuery.timeago(newComment.postedAt) + "</span>"
    html += "<p>" + markedMessage  + "</p></div>";
    
    $("#comments-container").prepend(html);
});

child_added returns a snapshot of the comment data into snapshot.val(). Then format it into HTML and prepend the result into the comments-container.

For styling the comments, Showdown JS is used to convert markdown message into HTML. And timeago JS to convert the timestamp into readable string. Don’t forget to include these two libraries.

Ads by Google

Safety issues

As I’m a newbie in this field, it’s my first time that encounters such issues as Cross-site Scripting (XSS) attack.

From Showdown’s wiki:

Cross-side scripting is a well known technique to gain access to private information of the users of a website. The attacker injects spurious HTML content (a script) on the web page which will read the user’s cookies and do something bad with it (like steal credentials). As a countermeasure, you should filter any suspicious content coming from user input. Showdown doesn’t include an XSS filter, so you must provide your own. But be careful in how you do it…

After a quick test, I found XSS is a problem on my site if scripts involved in the comments. Fortunately, there are several JavaScript implementation can filter XSS contents, showdown-xss-filter is such a accessible extension to filter XSS.

So, include the dependency to the XSS filter and update our JavaScript in previous section:

var converter = new showdown.Converter({ extensions: ['xssfilter'] });

That’s all for the XSS filter. But we still need to care about the data safety that stored in Firebase.

Since everyone is allowed to post comments without login, we have to set up some data rules to prevent deleting:

{
  "rules": {
    ".read": true,

    "$title": {
        "$slug": {
            ".write": "!data.exists()",
            "$message": {
              ".write": "!data.exists() || !newData.exists()"
            }
        }
    }
  
  }
}

For the !data.exists() || !newData.exists() rule setting, we can write as long as old data or new data does not exist.

Summaries

Well, our Firebase comment system is ready to use. The JS libraries used are:

<script src="https://cdn.jsdelivr.net/npm/jquery@3.2.1/dist/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/timeago@1.6.7/jquery.timeago.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/showdown@1.9.1/dist/showdown.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/showdown-xss-filter@0.2.0/showdown-xss-filter.min.js"></script>
<script src="/assets/js/showdown-xss-filter.js"></script>
<script src="/assets/js/comment.js"></script>

In our comment.js, the complete scripts are:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
$.getScript("https://www.gstatic.com/firebasejs/3.4.0/firebase.js", function () {
    var config = {
        apiKey: '<your-api-key>',
        authDomain: '<your-auth-domain>',
        databaseURL: '<your-database-url>',
        storageBucket: '<your-storage-bucket>'
    };
    firebase.initializeApp(config);
    var rootRef = firebase.database().ref();
    var postComments = rootRef.child('postComments');
    var link = $("link[rel='canonical']").attr("href");
    var pathkey = decodeURI(link.replace(new RegExp('\\/|\\.', 'g'),"_"));
    var postRef = postComments.child(pathkey);
    $("#comment").submit(function() {
        postRef.push().set({
            name: $("#name").val(),
            message: $("#message").val(),
            md5Email: md5($("#email").val()),
            postedAt: firebase.database.ServerValue.TIMESTAMP
        });
        $("input[type=text], textarea").val("");
        return false;
    });    
    postRef.on("child_added", function(snapshot) {
        var newComment = snapshot.val();
        var converter = new showdown.Converter({ extensions: ['xssfilter'] });
        converter.setFlavor('github');
        var markedMessage = converter.makeHtml(newComment.message);
        var html = "<div class='comment'>";
        html += "<h4>" + newComment.name + "</h4>";
        html += "<div class='profile-image'><img src='https://www.gravatar.com/avatar/" + newComment.md5Email + "?s=100&d=retro'/></div>";
        html += "<span class='date'>" + jQuery.timeago(newComment.postedAt) + "</span>"
        html += "<p>" + markedMessage  + "</p></div>";
        $("#comments-container").prepend(html);
    });
}

The above method might still working, but you should probably go though the updated Cloud Firestore documents that use the new Firestore instead of Firebase real-time database.

THE END
Ads by Google

林宏

Frank Lin

Hey, there! This is Frank Lin (@flinhong), one of the 1.41 billion . This 'inDev. Journal' site holds the exploration of my quirky thoughts and random adventures through life. Hope you enjoy reading and perusing my posts.

YOU MAY ALSO LIKE

Using Liquid in Jekyll - Live with Demos

Web Notes

2016.08.20

Using Liquid in Jekyll - Live with Demos

Liquid is a simple template language that Jekyll uses to process pages for your site. With Liquid you can output complex contents without additional plugins.

Hands on IBM Cloud Functions with CLI

Tools

2020.10.20

Hands on IBM Cloud Functions with CLI

IBM Cloud CLI allows complete management of the Cloud Functions system. You can use the Cloud Functions CLI plugin-in to manage your code snippets in actions, create triggers, and rules to enable your actions to respond to events, and bundle actions into packages.

Setup an IKEv2 server with strongSwan

Tutorials

2020.01.09

Setup an IKEv2 server with strongSwan

IKEv2, or Internet Key Exchange v2, is a protocol that allows for direct IPSec tunnelling between networks. It is developed by Microsoft and Cisco (primarily) for mobile users, and introduced as an updated version of IKEv1 in 2005. The IKEv2 MOBIKE (Mobility and Multihoming) protocol allows the client to main secure connection despite network switches, such as when leaving a WiFi area for a mobile data area. IKEv2 works on most platforms, and natively supported on some platforms (OS X 10.11+, iOS 9.1+, and Windows 10) with no additional applications necessary.