For a long time, I’ve wanted comments for my Jekyll-based ‘blog. However, the existing alternatives, Disqus and Staticman, were not satisfactory, so I decided to roll up my own implementation. It has certain drawbacks that might make it unsuitable for most applications, but I would like to tell its story here anyway, because it would not have been possible without the ‘blog posts of others.

I can’t say in advance how good of a story this makes, but I do have a fair amount of code to share with you, to help you get off your feet too. Obviously, not the full code—I’m not entirely convinced anyone else should actually literally follow in my footsteps and choose this as the basis for their comments engine—but enough that you’ll know what to do if you were me at the time I started: you know how programming works in general but you don’t know how these tools work in specific, and you don’t necessarily have the patience to learn them all the way through just to get a couple of measly comments on your ‘blog.

The technologies I’ll be using are ‘blog-aware, static website generator Jekyll—which is written in Ruby but has its templates written in a combination of HTML markup, SCSS styles, and Liquid templating—and famously maligned garbage fire PHP, which are as of this writing being served on Apache. Any compatible combination of static site generator, CGI-ready language, and CGI-capable HTTP server will do, but obviously you will have to more liberally translate the code snippets.


But let’s start from the beginning. Jekyll is a static website generator, which means its goal is to take templates and content and combine them into static webpages, which are then served by the HTTP server. The benefit of static web content is that it can be rather confidently cached, as it is not expected to change frequently. This reduces load on your HTTP server in two ways: firstly, it doesn’t need to do any thinking when serving the content, and secondly, it is highly amenable to caching by other services, which handles requests before they even reach it. And ‘blogs are some of the best suited to this publishing paradigm, because they consist almost entirely of content that rarely changes. Sure, you might correct some typoes or release updates to posts, and maybe you update frequently, but there isn’t really any significant computation needed at serving time. Thence come static site genrators like Jekyll.

However, being a static website means that any interactive facets of websites are off the table, unless they are somehow adapted to the static paradigm, because you can’t be both dynamic and static. For ‘blogs, this is the elephant in the room… on the table… because since the invention of comments in 1998, they have become a mainstay on ‘blogs specifically and content-based websites more generally. It is not that weird for one ‘blog to not have comments, but if Jekyll claims to be “‘blog-aware”, then it needs to have an answer to the inherently dynamic nature of comments.

Jekyll’s default solution is to outsource comments to Disqus, a comment hosting service. The ‘blog itself remains static, and each commentable page contains a widget—which is to say, an element that is dynamic only on the client-side—that defers the work of handling comments to the service. Features like comment moderation, user verification, and spam protection are handled either by Disqus or through it by logging in to an admin panel. For most people, this is probably the simplest and easiest solution, as it hides all the dynamic components away from the ‘blogger.

As is probably obvious from the historical irony of the existence of this ‘blog post, I did not find this solution satisfactory. A petty reason is that the design is ugly. It’s an ugly widget. Using your eyes on it is a negative experience. But here’s a more pressing reason. Because the comments are hosted on Disqus, they belong to Disqus. If Disqus goes down, your comments go down. To use the comments (as commentor or as ‘blogger) you need to agree to the Disqus Terms of Service, which include a User ToS, a set of content rules called the Service Rules, a Privacy Policy, an arbitration agreement, and a further Publisher ToS for the ‘blogger.1 No thanks. If I’m going to have comments, they will be mine.

"Do not sell my data" Disqus button

I saw this button at the bottom of a Disqus comments widget. Why does this button exist?

Professional website person Phil Hawksworth made a related but only somewhat similar observation: Disqus has way too many features, and an old school web hacker like him would much prefer a minimalist form service, which by the way can generalize the scope of “comments” to other user-generated content, like reviews or whatever. So in 2014 he created a project called Poole, which I won’t link because it was short-lived and is now defunct. My specific complaint surfaced in a talk in 2015 from the mouth of Tom Preston-Werner, cofounder of Jekyll and also Github, although in more moral terms. Ultimately Preston-Werner proposed that the future of comments on static sites was packaged alongside your ‘blog data and built by some omnipresent Jekyll-running service that also builds your ‘blog any time any other change occurs, such as a commit from your computer.

Fellow ‘blog theorist Eduardo Bouças, also following these developments, built a prototype system which looked to be the start of exactly that: it receives user comments via some minimal dynamic webpage (in his original case a PHP script), saves them as Jekyll-compatible data (the markup lingua franca is YAML), and then rebuilds your static website with the new content. If you want a moderation step, then instead of building immediately you log it somewhere and wait for the owner to respond, etc. The main point is this calculated violation of staticness.

Okay, that’s pretty good. I like it.

Eventually Bouças matures this project and bundles it into a service called Staticman, which could be either installed and run locally on your server, or if you use Github Pages, exists somewhere in the ephemeral cloud as an easily integrated plugin which you can grant permission to commit to your ‘blog repository on your behalf. And this is where the story turns sour for me: I’m not on Github Pages, so that’s out, and the local installation options are either npm up a server or something (kiiind of not interested) or install Docker and Docker Compose just to run its little bundled container (Suuuuper Not Interested In That 64 & Knuckles).

I get that this strategy kind of relies on there being a server-like entity that can monitor incoming submissions and trigger the rebuild, but honestly, if this is what that takes, I’ll look for something else. That said, I don’t really think there is something else. But what there is is a lineage—an absolute dearth—of ‘blog posts from Bouças and others on how they built their own comment engines in the static framework that I can adapt to my own purposes. It’s practically a trope for a coder’s personal Jekyll ‘blog to include a post on how you too can add static comments to your static ‘blog in exactly the same way as they did. It’s CONTENT, dude, you can’t pass up CONTENT. Well, who am I to break tradition?2

In the end, I decided on the following setup. Like Bouças, my solution to comments will be to violate the static constraint in the form of a PHP script accepting comments. Unlike Bouças, however, I am under no pressure to publish comments immediately upon submission, because I would like to moderate them. I have no particular qualms about manually checking for and moderating comments, either, because I don’t expect to receive very many for now, so I don’t even need any sort of tool to notify me externally. Although… one would certainly be convenient. Maybe someday I’ll hack together something that sends me an email when I get a comment.

I am also not interested in most comment innovations, so I won’t be including those. Nested replies, for instance, are temporal aberrations that belong only in large fora, and organizing and implementing such a system is far more work than I care for. Replying to people is fine, sure—I’ll include some way to link to previous comments and commentors—but I like the flow of time the way it is. I think the one thing that falls in this category is permitting formatting: I’ll allow full Markdown and MathJax in comments, and simply moderate anything that gets out of hand.

In fact, since I intend to go through these comments manually every time anyway, I’ve decided to rethink the purpose of comments a little bit. Sure, maybe I’m being a little sentimental here, thinking that I have a legitimate thing to iterate upon in this twenty-year old feedback mechanism. But at the very least, it’s an experiment I want to try.

The innovation I am proposing is the private comment. When you submit a comment, you have the option of ticking a checkbox that says “private”, and if you do, then when the comment is submitted, I’ll know it was meant for me only. It won’t be put in the publishing queue, and even if I ever decide to stop moderating comments, it won’t find its way onto the website. I imagine this to be a low-effort alternative to sending me an email, so that you don’t even have to leave the article you’re reading to contact me. I also hope there might be a secondary effect of implying these comments are meant to be a legitimate line of contact with me, and not just some functionality tacked onto the end of a ‘blog out of obligation or audience engagement or whatever.


Now let’s get to the implementation details. If you don’t care about that, then this is as good a place as any to stop reading, because what follows is all code snippets and explanations of Jekyll’s internal mechanisms. For everyone that does care but isn’t using the specific technologies I’m using, I’ve tried to insert some context so that you’ll have a handier time translating this into your own setting, but I should once again advise against following directly in my footsteps, because this was a rather idiosyncratic setup that was customized to match the specific way my brain is broken. There are likely saner solutions to this problem that you are perfectly comfortable with, and I encourage you to seek those out.

First of all, the architecture. The relevant fraction of Jekyll’s working directory/mental model looks something like the following. There’s a _posts/ folder full of Markdown files containing ‘blog posts, which are each passed through a Markdown parser and each fed into a post template _layouts/post.html. There is also a _data/ folder where you can leave data files for Jekyll to parse and make available in its templating language Liquid. Finally, there are some other miscellaneous files at the top level, which can include Markdown, HTML, SCSS, or other files that can specify tools they need for preprocessing (Markdown, Liquid, Sass, etc.) and optionally what layout in _layouts/ they desire to be set in.

I desire comments only on select ‘blog posts, and not at all on any other pages, so my comments engine will interface with Jekyll roughly as follows. I have a bespoke PHP script commentor.php, acting as a dynamic page which is executed via CGI, expecting a POST request with comment data. This saves the comment to a YAML file in _data/comments/, so that Jekyll can parse them and make them available to Liquid. I specify in each ‘blog post’s metadata whether or not it is to have comments, and if so, which comments file is desired. Finally I modified post.html to loop through and display the comments attached to a ‘blog post, if any exist, as well as a form for comment submission to commentor.php, if the post permits it. The remainder of the system—moderation, publishing—is handled manually, by me, via SSH or an SCP client. Specifically, rebuilding will publish public comments by default, but I can check a log to see if there’s been anything new.

Is it stupid? Yes. Does it work? Also yes. I’m a simple man.

Now, let’s get into the nitty-gritty. commentor.php looks like this:

<?php // commentor.php

/** expected CGI variables:
 *  $_SERVER['REMOTE_ADDR'] = ip
 *  $_POST['submit'] = button
 *  $_POST['location'] = honeypot
 *  $_POST['page'] = 'blog page
 *  $_POST['name'] = username
 *  $_POST['contact'] = email or whatever
 *  $_POST['private'] = private? checkbox
 **/

$date = time();
// initialize $ipbans
$ipbanned = in_array($_SERVER['REMOTE_ADDR'], $ipbans);

if(!$_POST['submit']) {
    $outcome = "form misuse";
}
elseif($ipbanned) {
    $outcome = "banned";
}
elseif(!empty($_POST['location'])) {
    $outcome = "spam";
}
elseif(empty($_POST['name'])) {
    $outcome = "no name";
}
else { // empty honeypot
    $outcome = "write attempt";

    // initialize $comments_file
    $public = $_POST['private'] ? "false" : "true";
    // $message = sanitized $_POST['message']
    $comment = <<<HEREDOC
...
HEREDOC;
    if(file_put_contents($comments_file, $comment, FILE_APPEND)) {
        $outcome = "success";
    }
    else {
        $outcome = "write fail";
    }
}

// log $outcome and stuff somewhere (optional)
// initialize e.g. $title, $body, ...

?><!DOCTYPE html>
<html>
<head><title><?= $title ?></title></head>
<body><?= $body ?></body>
</html>

It’s half code and half pseudocode, but that’s okay because you probably shouldn’t do this (and I probably shouldn’t be giving out all the details). One fun thing to notice is the rudimentary spam protection, in the form of IP bans and honeypots. If you want or need more comprehensive solutions, you can make that more sophisticated, but this works for me.

The comment itself (what you assemble in the HEREDOC) looks like this:

- page: blog-post
  date: 1234567890
  name: Dwigt Rortugal
  contact: [email protected]
  ip: 69.420.19.94
  public: !!bool true
  message: >
    cool blog very good

These would get appended to an appropriate .yml file in _data/comments/, so that it has a list of comments in it. When building, Jekyll will parse them and then you can access their data as {{ site.data.comments }} in Liquid. Using that, to every page in _layouts/ which is intended to display comments, you could add something like this, to print out all the comments:

<!-- comment displayer -->
{% if page.viewcomments %}
    <h1>Comments ({{ site.data.comments[page.comments] | where: "public" | size }} public)</h1>
    {% for comment in site.data.comments[page.comments] %}
        {% if comment.public %}
            <div class="comment" id="c{{ comment.date }}">
                <!-- comment no. {% increment counter %}  -->
                <p class="comment-author">{{ comment.name }} at {{ comment.date | date: "%R, %-d %b %Y" }}:</p>
                <div class="comment-body">{{ comment.message }}</div>
            </div>
        {% endif %}
    {% endfor %}
{% endif %}

Finally, the most important part, the form that actually receives the comments and pushes them out to commentor.php.

{% if page.sendcomments %}
  <h2>Submit a comment</h2>
  <form id="comment-form" action="{{ 'commentor.php' | relative_url }}" method="post">
    <div class="first-line">
      <input type="hidden" name="page" value="{{ page.commentspage }}" />
      <input type="hidden" name="id" value="{% increment counter %}" />
      <input type="text" name="location" class="honeypot" autocomplete="off" />
    </div>
    <div class="line">
      <input type="text" name="name" placeholder="Name" required />
      <span>Your name</span>
    </div>
    <div class="line">
      <input type="text" name="contact" placeholder="Email address"/>
      <span>An optional email (private)</span>
    </div>
    <div class="line">
      <textarea id="commentmsg" rows="10" cols="60" name="message" placeholder="Comment" required></textarea>
     </div>
    <div class="line">
      <input type="submit" name="submit" value="Send" />
      <span>Private comment?</span>
      <input type="checkbox" name="private" value="yes" />
    </div>
  </form>
{% else %}
  <h2 class="nothing-there">Comments have been disabled for this post.</h2>
{% endif %}

The way I wrote it, I can enable or disable comments on a per-post basis, both for viewing (page.viewcomments, page.commentspage) and submission (page.sendcomments), by adding or removing those fields in the Jekyll preambles of the posts.

At this point, the sky’s the limit. You can style this how you want, in assets/main.scss or wherever you’re keeping your styles. Here’s a hot tip to get you started: you can strip the comment body, like {{ comment.message | strip }}, and then style it with white-space: pre-line; so that it preserves people’s newlines internally but cleans up any starting or ending whitespace. Jekyll also extends Liquid with a markdownify filter so you can enable markdown in comments by running it through that.

You want another tip? Sure, why not? If you want to make replying easier, then give each comment an id (I used c{{comment.date}} in my example above) and then also throw an onclick attribute on the name, containing some Javascript to append a link to the comment:

document.getElementById('commentmsg').value += '@[{{comment.name}}][#{{comment.id}}] '

Like I said, the sky’s the limit. Well, that and your webdesign skills, I suppose. And your sense of prudence. And your tolerance for bad ideas. There are a lot of limits, actually. Whatever.


As I conclude, I would like to remind you once more why it is a bad idea to implement comments the way I did.

For one, there is no notification system. All comments go through me and their timely publishing relies on me to check for them. If you’re interested in engagement or public discourse or whatever then this is terrible because that means nothing can happen independent of your presence. Even from a UX perspective this is kind of weird, because the only feedback that a comment was received is the confirmation page, and the comment doesn’t show up. Corrective action for this looks like some kind of notification system, possibly via email. I am loosely interested in this, because the problem of deciding when to notify seems like an interesting one, but it is not pressing, because I don’t think the downsides to the system as-is are that awful.

For another, the specific details of this implementation mean that people can sneak comments in if they comment right as I am building the website for an unrelated change. This is fairly easily addressed, simply by adding a field for whether a comment has passed moderation. I don’t expect voluminous comments, but this seems fairly easy to implement, and I can’t really think of a good reason for not going back and doing it now besides “I don’t feel like it”?

Finally, it’s a lot of work for something as simple as comments. Just sell your soul to Disqus or Github for the convenience. I gather that indifference about corporate contracts is pretty hip. Maybe I’m sounding too cynical: as you can imagine I have pretty strong opinions about this stuff. I don’t have a Facebook account even though I am in principle a member the generation for whom it was hip to use Facebook in high school. (Now I hear it’s Boomer central and all the cool kids are on Twitter Instagram LinkedIn? TikTok. IDK, I don’t keep up with social media demographics.) Regardless, this may still be a valid reason for some people not to follow this implementation.

Upon reflection, I actually don’t know how that last one might be the problem for you if the previous two were okay. Maybe you’re a younger version of me, and knowing that something is stupid isn’t going to stop you from doing it? That’s probably as good a signal as any that I should stop trying to come up with stuff to write.

So that’s the end of my ‘blog post. If you liked this ‘blog post, give it a Like, or in some cases preferably a Dislike, so that The Algorithm will increase my Exposure to and Impressions on Suitable Demographics. If you haven’t already inflated my famousness number yet, you should definitely subscribe and activate all of the related notification options, so that a corporation can see you prostrating on its altar and be content, however fleetingly, with your obsequience.

And, of course, leave a comment below! 😉

  1. Here’s some of the fine print in the Disqus Terms of Service, specifically out of the User ToS (UToS), Publisher ToS (PToS), and Privacy Policy (PP). Disqus owns the comments (PToS§4.1) and can give them to whoever it wants (PToS§4.2, PP§§2.b.iv, 5.d.iv-vii, 6). Users grant (and must be able to grant) Disqus license to all creative rights to their comments (UToS§Rights Regarding User Content) except as is strictly unenforcable by intellectual property law (PToS§5). Your own use of your website’s comments, analytics, and moderation is on a revocable license (PToS§4.1) and you have to relinquish and/or destroy all of it at Disqus’ request (PToS§4.2), not to mention Disqus is under no obligation to give any of it to you if the agreement is terminated (PToS§10.2). Disqus can use your ‘blog’s branding in its marketing materials (PToS§11.8) and you have to notify them in writing if you do not agree to this, or any of the other handful of things you are legally allowed to disagree to. 

  2. Yeah, yeah, I know there’s no level of self-awareness I can mount to excuse my own participation in this sordid tradition. But I think I understand what this kind of post means to someone, symbolically. For me this post is also: an announcement to everyone that my ‘blog now has comments; a proof to myself that I can still make computers do what I want and that I will not be bested by technology; and most importantly closure for a long-standing project. So I suppose I should be willing to extend this courtesy to all the bandwagoners and not just the truly innovating ones.