I recently migrated this blog from WordPress to Jekyll. After doing the basic content migration and a few minor layout tweeks I wanted to get comments working. Since Jekyll simply renders HTML templates and Markdown into static files there is no commenting system. For simplicities sake I decided to embed Disqus.

Disqus is a popular hosted comment service. Most of the dynamic blogging platforms have plugins that make the configuration dummy proof, but it can be used on any website with little more than pasting in some JavaScipt. They even have some minimal instructions for for Jekyll. In this post I’m going to walk through my implementation and explain a bit about what’s happening.

Get your Disqus Shortname

The first thing you’ll want to do is sign up for a Disqus account and setup a website. You’ll then need to determine the sortname Disqus has assigned to your site. Go to the site admin page, and look for Shortname under Site Identity. In my case the shortname is “vcabbage.” Take note of your shortname, we’ll need it shortly so that Disqus knows which site to associate the comments with.

Configure the Comments

Now we’ll move on to the adding the comments to the post.html template. The Disqus instructions suggest adding a comments: [true|false] parameter to the Front Matter on each post so that comments can be selectively enable/disabled on each page. I like this approach and implemented it verbatim.

---
layout: post
title:  "Implementing Disqus with Jekyll"
date:   2015-06-21 16:08:00
categories: development
comments: true
---

Diving into the Script

Next up is inserting the JavaScript. I’m going to go through the snippet pretty throughly here, you can skip down to Implementing the Script if you’re not interested.

Here’s the code Disqus provides:

<div id="disqus_thread"></div>
<script type="text/javascript">
    /* * * CONFIGURATION VARIABLES * * */
    var disqus_shortname = 'vcabbage';
    
    /* * * DON'T EDIT BELOW THIS LINE * * */
    (function() {
        var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
    })();
</script>
<noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript" rel="nofollow">comments powered by Disqus.</a></noscript>

I don’t dig the formatting on this, so let’s live dangerously and ignore the “DON’T EDIT BELOW THIS LINE” and clean it up a bit.

<div id="disqus_thread"></div>
<script type="text/javascript">
  /* * * CONFIGURATION VARIABLES * * */
  var disqus_shortname = 'vcabbage';

  /* * * DON'T EDIT BELOW THIS LINE * * */
  (function() {
    var dsq = document.createElement('script');
    dsq.type = 'text/javascript';
    dsq.async = true;
    dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
    (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
  })();
</script>
<noscript>
  Please enable JavaScript to view the
  <a href="https://disqus.com/?ref_noscript" rel="nofollow">
    comments powered by Disqus.
  </a>
</noscript>

That’s better. So what’s going on here? First there’s an empty <div id="disqus_thread"></div>. While it’s not shown in this snippet, eventually the Disqus comments box will be be inserted inside of this <div>.

Inside the <script> tags we have the variable definition var disqus_shortname = 'vcabbage';. This is going to end up being a global variable since it’s not contained within the IIFE below. Not ideal, but since other scripts on the page are going to need access to it it’s a bit of a necessity.

Now we get to the IIFE (immediately invoked function expression, pronounced iffy), for those unaware and want the TL;DR version, an IIFE is an anonymous function which is called immediately. Sounds kind of useless, right? You could just write the statements outside a function. The problem is that in JavaScript any variable decalared outside a function is a global variable and we could end up with conflicts with other scripts. Wrapping the definitions in an IIFE keeps all the variables within the scope of the anonymous function.

  • On the next line we see a new <script> element being created and assigned to the variable dsq. At this point the element has been created in memory, but hasn’t been inserted into the page’s DOM.
var dsq = document.createElement('script');
  • Next, the element is identified as containing JavaScript code. At this point the element looks like <script type="text/javascript"></script>.
dsq.type = 'text/javascript';
  • The async paramenter is added, telling the browser to pull in the script asynchronously, then execute it. Without this attribute the browser would wait for the script to be downloaded and executed before rendering the rest of the page. Since we usually want visitors to see the blog article content as soon as possible and are less concerned about comments, this can improve the percieved load time of the page. Now the element is <script type="text/javascript" async="true"></script>.
dsq.async = true;
  • The penultimate line sets the URL of the script that will be pulled in. The disqus_shortname is being inserted into the URL. One thing some may not be used to is the // without a resourse type. This tells the browser to use whatever method the main page is being accessed by. If we were accessing the site via HTTPS the browser would pull the script from https://vcabbage.disqus.com/embed.js rather than http://vcabbage.disqus.com/embed.js. This prevents those “page contains secure and nonsecure items” warnings on HTTPS sites. The element becomes <script type="text/javascript" async="true" src="https://vcabbage.disqus.com/embed.js"></script>.
dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
  • Finally we have the rather dense line. The first section document.getElementsByTagName('head')[0] attempts to get the <head> element from the page. If there isn’t a <head> element it will return a falsey value and the || (or) condition will be evaluated, which retrieves the <body> element. Once it has one of the two elements, it will call the appendChild() method on the element with the <script> element that was just created. This has the affect of inserting the script within the <head> element (or the <body> if the is no <head>).
(document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);

At this point the browser will pull in an execute the referenced script, which will do all the magic that Disqus needs to work.

Implementing the Script

I’m going to make a couple modifications to this script. First, we’re going to want the disqus_shortname variable accessible to another snippet later on. To avoid repeating that line, I’m going to add it into the _includes/head.html file, right before the </head> tag.

<script type="text/javascript">
      var disqus_shortname = 'vcabbage';
  </script>
</head>

Next I’m going to add a disqus_shortname shortname setting into my _config.yaml file and replace the hardcoded vcabbage with a Jekyll variable.

_config.yaml:
# Site settings
title: vCabbage
email: kale@vcabbage.com
description: > # this means to ignore newlines until "baseurl:"
  Nutrition for the network.
baseurl: "" # the subpath of your site, e.g. /blog/
url: "http://vCabbage.com" # the base hostname & protocol for your site
twitter_username: vCabbage
github_username:  kalebksp
linkedin_username: kaleblankenship
disqus_shortname: vcabbage

# Build settings
markdown: kramdown
_includes/head.html:
<script type="text/javascript">
      var disqus_shortname = '{{ site.disqus_shortname }}';
  </script>
</head>

Now the templating engine will insert the value from _config.yaml and we can keep our site configuration all in one place.

In _layouts/post.html we’ll add the this block, after the </article> tag.

{% if page.comments %}
  <span id="disqus">
    <div id="disqus_thread"></div>
    <script type="text/javascript">
      var disqus_title = '{{ page.title }}';
      var disqus_url = '{{ page.url | prepend: site.url }}';

      (function() {
        var dsq = document.createElement('script');
        dsq.async = true;
        dsq.type = 'text/javascript';
        dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
        (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
      })();
    </script>
    <noscript>
      Please enable JavaScript to view the
      <a href="https://disqus.com/?ref_noscript" rel="nofollow">
        comments powered by Disqus.
      </a>
    </noscript>
  </span>
{% endif %}

You may notice that I’ve wrapped the entire block in a <span id="disqus">, I’ll come back to this in the comment counts section.

I’ve also added two new global variables.

disqus_title explicitly sets the discussion title to the current article’s title.

disqus_url sets the discussion URL to the page URL prefixed by the URL configured in _config.yaml. This is important if you preview your content locally by running jekyll serve and you want the comments to work.

Disqus tracks comment threads by the page URL and if I preview my content at http://localhost:4000/date/article-name a new thread will be created for that URL rather than http://vCabbage.com/date/article-name. Setting disqus_url to http://vCabbage.com/date/article-name prevents this problem.

You should now have working comments on your blog. Disqus also provides a method to insert comment counts, we’ll explore this next.

Configure the Comment Counts

The process to add comment counts is similar to adding comments to the blog posts. Since the JavaScript is nearly identical, I’m not going to go in-depth with my explaination.

These instructions will apply comment counts to each article listed on the main page.

First, we’ll add this prettified version of the comment count code at the end of the index.html page.

<script type="text/javascript">
  (function () {
    var s = document.createElement('script');
    s.async = true;
    s.type = 'text/javascript';
    s.src = '//' + disqus_shortname + '.disqus.com/count.js';
    (document.getElementsByTagName('HEAD')[0] || document.getElementsByTagName('BODY')[0]).appendChild(s);
  }());
</script>

The only differences here is that now we’re loading the count.js script rather than embed.js. We don’t need to define any variables since disqus_shortname was defined in <head>.

Now we just need to add a tag that count.js can parse and replace with the comment counts for each article. This is the tag I’m using is:

<a href="{{ post.url | prepend: site.baseurl }}#disqus" data-disqus-url="{{ post.url | prepend: site.url }}"></a>

I didn’t put any content in the anchor tag so that it won’t display on initial load. The count.js script will insert the comment counts into the tag after it loads.

The data-disqus-url attribute tells the count.js script to look up the thread by the supplied URL, which matches what we defined in the disqus_url on the blog post pages.

When clicked, the link will go to the <span id="disqus"> tag on the blog post’s page. While the data-disqus-url will reference the public site’s URL, this link will reference where ever the site happens to be loaded from, whether it be previewed locally or deployed to a server.

We can’t use #disqus_thread as our anchor, otherwise count.js would have automatically used the href URL rather than data-disqus-url and comment counts wouldn’t work when previewed locally.

Wrapping it Up

You should now have working comments on your site. If you have any questions or improvements, leave me a comment or message me on twitter @vCabbage.