Hosting comments on GitHub for static websites

Posted by on

I was in the middle of the development of my new website when I decided that I wanted to allow comments on my posts. That’s not an easy task if like me you have decided to go for a static website as you would need some sort of backend on a different server or to rely on a third party service. I started looking for various options, from services like Disqus which didn’t give me the flexibility I wanted to a combination of AWS services like Lambda + SNS + Google OAuth which would have been way too complicated for something like this, until I figured: why don’t I use GitHub issues? I already have a repository for this website hosted there, I’d just need to to fetch the comments in Javascript using the GitHub APIs, plus almost everyone to whom this blog is addressed has a GitHub account.

In this post I will show you how to host comments with GitHub issues, I will try to give you a general idea and some examples so you will be able to implement it for any static site.

Analyzing GitHub’s issue tracker

We will have to manually create a new issue for each post we make, this could be automated but in my opinion it isn’t quite worth the effort. If you haven’t yet, GitHub issues can be enabled in the repository settings and they will all share the same base URL (https://github.com/daniele-salvagni/dan.salvagni.io/issues/ in my case) followed by a progressive ID of the specific issue. As an example, the ID of the issue relative this post is 1.

GitHub’s API give us a nice way to get all the comments relative to an issue. Here is all we need to know:

Here is an example of how the request should look like:

Request URL:
https://api.github.com/repos/daniele-salvagni/dan.salvagni.io/issues/1/comments
Request Method: GET
Accept: application/vnd.github.VERSION.html+json

Here is a gist containing the response: response.json

Adding an issue ID to our posts

I’m using Metalsmith to generate this website but it won’t matter too much as most static site generators will possess a way to include arbitrary metadata in a specific page, we want to put our issue ID there. In Metalsmith you tipically use markdown files to write the content of the pages with some YAML-front-matter information. The header of this blog post source looks something like this:

---
title: Hosting comments on GitHub for static websites
collection: blog
layout: article.hbs
issue: 1
---
Lorem ipsum dolor sit...

We can now use this ID in our semantic template engine (Handlebars in my case) to link the users to the correct issue page:

Want to leave a comment? Visit
<a href="https://github.com/daniele-salvagni/dan.salvagni.io/issues/{{issue}}">
  this post's issue page
</a>
on GitHub, it will show up here!

Retrieving the comments with Javascript

This can be done in many ways, you could make an XHR and render it with just vanilla javascript or use jQuery.ajax() if you are already using jQuery on your website. Loading jQuery just for this isn’t probably worth it, I suggest something like the axios library instead (the one I’m using here) if you don’t want to deal manually with XMLHttpRequest.

About the Same Origin Policy

The Same Origin Policy is a security mechanism to prevent a potentially malicious script loaded from one origin (domain) to interact with a resource from another origin (thus obtaining access to sensitive data).

The most common way to “bypass” this mechanism has always been to use JSONP (and that’s what I did try initially), the problem is that JSONP doesn’t allow us to set a custom header to our requests. This is required by the GitHub APIs to set the HTML media type (by using application/vnd.github.VERSION.html+json) in order to return the HTML rendered from the comments markdown instead of just the raw data (otherwise we would need to render the Markdown ourselves).

However, GitHub supports the CORS mechanism which enables cross-domain data transfers. Until some time ago you had to register your website as an OAuth application on GitHub (as I found out on this post by Ivan), however CORS is now enabled for any origin.


After this little digression let’s get back to Javascript! Here is how we would get all the comments (for examples with the axios library):

let issueNum = 1; // Retrieve the issue number for your post here

let instance = axios.create({
    baseURL: 'https://api.github.com',
    timeout: 5000,
    // Set the correct media type
    headers: {'Accept': 'application/vnd.github.VERSION.html+json'}
});

instance.get('/repos/daniele-salvagni/dan.salvagni.io/issues/'
    + issueNum + '/comments')
.then(response => console.log(response.data))
.catch(function (error) {
  console.log(error);
});

This will print in our console a list of objects, each containing all the data relative to a comment. Now you just need to iterate on response.data and render the HTML. I’ll give you a brief idea on how I’m doing this with Vue in a moment. The following is a list of some properties you will most likely need, data being the list of objects you received as a response:

data[i].body_html: "The comment body rendered in HTML"
data[i].created_at: "The timestamp of the comment"
data[i].user.login: "The username of the comment author"
data[i].user.avatar_url: "The profile picture of the comment author"
data[i].user.html_url: "The GitHub profile URL of the comment author"

You can resize the avatar by appending &s=[size] to the URL (replace [size] to a number of pixels like 100).

Rendering the HTML with Vue

I’m rendering the comments with Vue.JS (you don’t need to!) mainly because I was already familiar with Angular and I wanted to finally try it out after all the hype, plus it makes for a faster development while being easier to maintain than plain Javascript with a bunch of HTML in it thanks to the Model-View separation, I also plan to use it more extensively in the future.

In Vue I’m just adding the response from the axios call mentioned before as property to the data object. Then it is easy to iterate over the comments with a v-for directive.

<!-- The VueJS app, notice the issue number stored in the data-issue attribute -->
<div id="comments-app" data-issue="{{issue}}">
<!-- Render only if the post has an issue number in the metadata -->
{{#if issue}}
<!-- Prevent Handlebars from rendering this block (will be rendered by Vue) -->
{{{{raw}}}}
    <div class="comment" v-for="comment in comments">
        <div class="comment-avatar">
            <img v-bind:src="comment.user.avatar_url + '&s=80'">
        </div>
        <div class="comment-meta">
            <a class="comment-user" v-bind:href="comment.user.html_url">
            {{comment.user.login}}</a> commented on {{ comment.created_at }}
        </div>
        <!--Render the raw html of the comment body -->
        <div class="comment-body" v-html="comment.body_html"></div>
    </div>
{{{{/raw}}}}
    <div class="comment-write"><strong>Want to leave a comment?</strong>
    Visit <a href="{{site.issuePage}}{{issue}}">this post's issue page</a>
    on GitHub, it will show up here!</div>
{{/if}}
</div>

That’s it, GitHub’s API are doing all the work and it should be fairly simple to replicate this for any static site.


{{comment.user.login}} commented on {{ comment.created_at | truncate(10) }}

Want to leave a comment? Use this post's issue page on GitHub, it will show up here!