Improving Static Comments with Jekyll & Staticman

In the months after ditching Disqus for a static-based commenting system, Staticman has matured with feature adds like threaded comments and email notifications.

Armed with instructions provided by Eduardo Bouças in this GitHub issue, I set off to level-up the commenting experience on Made Mistakes. Here’s how I did it.

Upgrade to Staticman v2

To take advantage of the new features, it was necessary to migrate Staticman settings from Jekyll’s _config.yml file into a new staticman.yml file1. None of the parameter names changed making the transition to v2 that much easier.

  allowedFields     : ['name', 'email', 'url', 'message']
  branch            : "master"
  commitMessage     : "New comment."
  filename          : "comment-{@timestamp}"
  format            : "yaml"
  moderation        : true
  path              : "src/_data/comments/{options.slug}"
  requiredFields    : ['name', 'email', 'message']
    email           : md5
      type          : "date"
        format      : "iso8601"

New Configuration Options

Be sure to check the sample configuration file and full list of parameters for setup ideas.

For example you can configure multiple properties (comments, reviews, and other types of user-generated content), change commit message and request body text, enable email notifications, and much more from a staticman.yml file.

Remove/Add Staticman as a Collaborator

I’m not entirely sure if doing the following was necessary. I encountered errors when submitting test comments and this appeared to solve the problem. It’s possible I mis-configured something else and that was the real issue…

Either way, let me know about your experience upgrading from Staticman v1 to v2 in the comments below.

  1. Revoked collaboration rights for Staticman v1 by removing from my GitHub repository. Remove staticmanapp as a collaborator
  2. Added Staticman back as collaborator.
  3. Pinged the version 2 endpoint{your GitHub username}/{your repository name} to accept the collaboration invitation.

Update POST Endpoint in Comment Form

To POST correctly to Staticman, the action attribute in my comment form needed a small update. Changing v1 to v2 in _includes/page__comments.html and appending /comments2 to the end did the trick for me.

<form id="comment-form" class="page__form js-form form" method="post" action="{{ site.repository }}/{{ site.staticman.branch }}/comments">

Add Support for Threaded Comments

Getting nested comments working was a big pain point for me. Numerous Liquid errors, trying to wrap my head around for loops inside of other for loops, array filters that broke things, and more — took me a bit to figure out.

Add “Replying To” Identifier

To properly nest replies I needed a way of determining their hierarchy. I went with a field named replying_to and added it as an allowedField to my Staticman config file:

allowedFields: ["name", "email", "url", "message", "replying_to"]

And to my comment form as a hidden field:

<input type="hidden" id="comment-parent" name="fields[replying_to]" value="">

Update: Field Name Change

After publishing this article I learned that options[parent] is meant to identify subscription entries, and not comment lineage. I’ve since changed to fields[replying_to] and updated the article and sample code to reflect this.

Update Liquid Loops

To avoid displaying duplicates, I needed to exclude replies and only top level comments in the main loop. This seemed like the perfect use-case for Jekyll’s where_exp filter:

Where Expression Jekyll Filter

Select all the objects in an array where the expression is true. Jekyll v3.2.0 & later. Example: site.members | where_exp: "item", "item.graduation_year == 2014"

If the hidden fields[replying_to] field I added to the form was working properly I should have comment data files similar to these:

Parent comment example

message: This is parent comment message.
name: First LastName
email: md5g1bb3r15h
date: '2016-11-30T22:03:15.286Z'

Child comment example

message: This is a child comment message.
name: First LastName
email: md5g1bb3r15h
replying_to: '7'
date: '2016-11-02T05:08:43.280Z'

As you can see above, the “child” comment has replying_to data populated from the hidden fields[replying_to] field in the form. Using this knowledge I tested against it using where_exp:"comment", "comment.replying_to == blank" to create an array of only “top-level” comments.

{% assign comments =[page.slug] | sort | where_exp: "comment", "comment[1].replying_to == blank" %}
{% for comment in comments %}
  {% assign avatar      = comment[1].avatar %}
  {% assign email       = comment[1].email %}
  {% assign name        = comment[1].name %}
  {% assign url         = comment[1].url %}
  {% assign date        = comment[1].date %}
  {% assign message     = comment[1].message %}
  {% include comment.html avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}
Parent comments only
Success, there be parent comments Captain!

Note: Sort and Where Filters Don’t Mix

I ran into strange behaviors and errors due to mixing a sort filter with where and where_exp. Came to the conclusion it was unnecessary as the items were already being sorted alphabetically based on their filenames, and removed the filter.

I’m using the following: filename: \"comment-{@timestamp}\" structure. Your mileage may vary depending on how you name entries.

Note: Added Back sort Filter

Not exactly sure if it’s a filesystem or OS thing, but building my site with Travis CI shuffled the order of comments. Applying sort to the comments assign was necessary to get everything in the correct chronological order.

Displaying Nested Comments

Here is what I was looking to accomplish… before the headaches started :anguished: :gun:

  • Start a loop and on each iteration create a new array named replies of only reply comments.
  • Evaluate the value of replying_to in these replies.
  • If replying_to is equal to the index of the parent loop then it’s a child and should be treated as one.
  • If not, move on to the next entry in the array.
  • Rinse and repeat.

I determined the easiest way of assigning a unique identifier to each parent comment would be sequentially. Thankfully Liquid provides a way of doing this with forloop.index.

{% assign index = forloop.index %}

Next I nested a modified copy of the “top-level comment” loop from before inside of itself — to function as the “child” or replies loop.

{% capture i %}{{ include.index }}{% endcapture %}
{% assign replies =[page.slug] | sort | where_exp: "comment", "comment[1].replying_to == i" %}
{% for reply in replies %}
  {% assign index       = forloop.index | prepend: '-' | prepend: include.index %}
  {% assign replying_to = reply[1].replying_to %}
  {% assign avatar      = reply[1].avatar %}
  {% assign email       = reply[1].email %}
  {% assign name        = reply[1].name %}
  {% assign url         = reply[1].url %}
  {% assign date        = reply[1].date %}
  {% assign message     = reply[1].message %}
  {% include comment.html index=index replying_to=replying_to avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}

Unfortunately the where_exp filter proved troublesome yet again, causing Jekyll to error out with: Liquid Exception: Liquid error (line 47): Nesting too deep in /_layouts/page.html.

After brief thoughts of the movie Inception, I applied an inspect filter to help troubleshoot the replies loop. I determined that the where_exp condition was failing3 because I was trying to compare an integer against a string :flushed:.

To solve this I placed a capture tag around the index variable to convert it from an integer into a string. Then modified the where_exp condition to compare replying_to against this new {{ i }} variable — fixing the issue and allowing me to move on.

{% capture i %}{{ include.index }}{% endcapture %}
{% assign replies =[page.slug] | where_exp:"item", "item.replying_to == i" %}


<section class="page__reactions">
  {% if site.repository and site.staticman.branch %}
    {% if[page.slug] %}
      <!-- Start static comments -->
      <div id="comments" class="js-comments">
        <h2 class="page__section-label">
          {% if[page.slug].size > 1 %}
            {{[page.slug] | size }}
          {% endif %}
        {% assign comments =[page.slug] | sort | where_exp: 'comment', 'comment[1].replying_to == blank' %}
        {% for comment in comments %}
          {% assign index       = forloop.index %}
          {% assign replying_to = comment[1].replying_to | to_integer %}
          {% assign avatar      = comment[1].avatar %}
          {% assign email       = comment[1].email %}
          {% assign name        = comment[1].name %}
          {% assign url         = comment[1].url %}
          {% assign date        = comment[1].date %}
          {% assign message     = comment[1].message %}
          {% include comment.html index=index replying_to=replying_to avatar=avatar email=email name=name url=url date=date message=message %}
        {% endfor %}
      <!-- End static comments -->
    {% endif %}

    {% unless page.comments_locked == true %}
      <!-- Start new comment form -->
      <div id="respond">
        <h2 class="page__section-label">Leave a Comment <small><a rel="nofollow" id="cancel-comment-reply-link" href="{{ page.url | absolute_url }}#respond" style="display:none;">Cancel reply</a></small></h2>
        <form id="comment-form" class="page__form js-form form" method="post" action="{{ site.repository }}/{{ site.staticman.branch }}/comments">
            <label for="comment-form-message"><strong>Comment</strong> <small>(<a href="">Markdown</a> is allowed)</small></label>
            <textarea type="text" rows="6" id="comment-form-message" name="fields[message]" required spellcheck="true"></textarea>
            <label for="comment-form-name"><strong>Name</strong></label>
            <input type="text" id="comment-form-name" name="fields[name]" required spellcheck="false">
            <label for="comment-form-email"><strong>Email</strong> <small>(used for <a href="">Gravatar</a> image and reply notifications)</small></label>
            <input type="email" id="comment-form-email" name="fields[email]" required spellcheck="false">
            <label for="comment-form-url"><strong>Website</strong> <small>(optional)</small></label>
            <input type="url" id="comment-form-url" name="fields[url]"/>
          <fieldset class="hidden" style="display: none;">
            <input type="hidden" name="options[origin]" value="{{ page.url | absolute_url }}">
            <input type="hidden" name="options[parent]" value="{{ page.url | absolute_url }}">
            <input type="hidden" id="comment-replying-to" name="fields[replying_to]" value="">
            <input type="hidden" id="comment-post-id" name="options[slug]" value="{{ page.slug }}">
            <label for="comment-form-location">Leave blank if you are a human</label>
            <input type="text" id="comment-form-location" name="fields[hidden]" autocomplete="off">
          <!-- Start comment form alert messaging -->
          <p class="hidden js-notice">
            <span class="js-notice-text"></span>
          <!-- End comment form alert messaging -->
            <label for="comment-form-reply">
              <input type="checkbox" id="comment-form-reply" name="options[subscribe]" value="email">
              Notify me of replies by email.
            <button type="submit" id="comment-form-submit" class="btn btn--large">Submit Comment</button>
      <!-- End new comment form -->
    {% else %}
      <p><em>Comments are closed. If you have a question concerning the content of this page, please feel free to <a href="/contact/">contact me</a>.</em></p>
    {% endunless %}
  {% endif %}


<article id="comment{% unless include.r %}{{ index | prepend: '-' }}{% else %}{{ include.index | prepend: '-' }}{% endunless %}" class="js-comment comment {% if == %}admin{% endif %} {% unless include.replying_to == 0 %}child{% endunless %}">
  <div class="comment__avatar">
    {% if include.avatar %}
      <img src="{{ include.avatar }}" alt="{{ | escape }}">
    {% elsif %}
      <img src="{{ }}?d=mm&s=60" srcset="{{ }}?d=mm&s=120 2x" alt="{{ | escape }}">
    {% else %}
      <img src="/assets/images/avatar-60.png" srcset="/assets/images/avatar-120.png 2x" alt="{{ | escape }}">
    {% endif %}
  <h3 class="comment__author-name">
    {% unless include.url == blank %}
      <a rel="external nofollow" href="{{ include.url }}">
        {% if == %}<svg class="icon" width="20px" height="20px"><use xlink:href="#icon-mistake"></use></svg> {% endif %}{{ }}
    {% else %}
      {% if == %}<svg class="icon" width="20px" height="20px"><use xlink:href="#icon-mistake"></use></svg> {% endif %}{{ }}
    {% endunless %}
  <div class="comment__timestamp">
    {% if %}
      {% if include.index %}<a href="#comment{% if r %}{{ index | prepend: '-' }}{% else %}{{ include.index | prepend: '-' }}{% endif %}" title="Permalink to this comment">{% endif %}
      <time datetime="{{ | date_to_xmlschema }}">{{ | date: '%B %d, %Y' }}</time>
      {% if include.index %}</a>{% endif %}
    {% endif %}
  <div class="comment__content">
    {{ include.message | markdownify }}
  {% unless include.replying_to != 0 or page.comments_locked == true %}
    <div class="comment__reply">
      <a rel="nofollow" class="btn" href="#comment-{{ include.index }}" onclick="return addComment.moveForm('comment-{{ include.index }}', '{{ include.index }}', 'respond', '{{ page.slug }}')">Reply to {{ }}</a>
  {% endunless %}

{% capture i %}{{ include.index }}{% endcapture %}
{% assign replies =[page.slug] | sort | where_exp: 'comment', 'comment[1].replying_to == i' %}
{% for reply in replies %}
  {% assign index       = forloop.index | prepend: '-' | prepend: include.index %}
  {% assign replying_to = reply[1].replying_to | to_integer %}
  {% assign avatar      = reply[1].avatar %}
  {% assign email       = reply[1].email %}
  {% assign name        = reply[1].name %}
  {% assign url         = reply[1].url %}
  {% assign date        = reply[1].date %}
  {% assign message     = reply[1].message %}
  {% include comment.html index=index replying_to=replying_to avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}

Comment Reply HTML and JavaScript

Next up was to add some finishing touches to pull everything together.

Familiar with the way Wordpress handles reply forms I looked to it for inspiration. Digging through the JavaScript in wp-includes/js/comment-reply.js I found everything I could possibly need:

  • respond function to move form into view
  • cancel function to destroy a reply form a return it to its original state
  • pass parent’s unique identifier to fields[replying_to] on form submit

To start I used an unless condition to only show reply links on “top-level” comments. I only planned on going one-level deep with replies, so this seemed like a good way of enforcing that.

{% unless r %}
  <div class="comment__reply">
    <a rel="nofollow" class="btn" href="#comment-{{ include.index }}">Reply to {{ }}</a>
{% endunless %}
Nested comments
Nested comments one-level deep.

To give the reply link life I added the following onclick attribute and JavaScript to it.

onclick="return addComment.moveForm('comment-{{ include.index }}', '{{ include.index }}', 'respond', '{{ page.slug }}')"

A few minor variable name changes to Wordpress’ comment-reply.js script was all it took to get everything working with my form markup.

Comment replies in action
Hitting a reply button moves the comment form into view and populates <input type='hidden' id='comment-replying-to' name='fields[replying_to]' value=''> with the correct parent value. While tapping Cancel reply returns the input to its original state of null.

Add Support for Email Notifications

Compared to nesting comment replies, email notifications were a breeze to setup.

Update staticman.yml Configuration

To ensure that links in notification emails are safe and only come from trusted domains, set allowedOrigins accordingly.


allowedOrigins: [""]

The domain(s) allowed here must match those passed from an options.origin field we’re going to add in the next step. Only domains that match will trigger notifications to send, otherwise the operation will abort.

ProTip: Use Your Own Mailgun Account

The public instance of Staticman uses a Mailgun account with a limit of 10,000 emails a month. You are encouraged to create an account and add your own Mailgun API and domain to staticman.yml. Be sure you encrypt both using the following endpoint:{TEXT TO BE ENCRYPTED}.

Update Comment Form

To finish, add the following three fields to the comment form.

Field 1 + 2: Hidden input fields that pass the origin4 set in staticman.yml and unique identifier to the entry the user is subscriber to:

<input type="hidden" name="options[origin]" value="{{ page.url | absolute_url }}">
<input type="hidden" name="options[parent]" value="{{ page.url | absolute_url }}">

Field 3: A checkbox input for subscribing to email notifications.

<label for="comment-form-reply">
  <input type="checkbox" id="comment-form-reply" name="options[subscribe]" value="email">
  Notify me of new comments by email.

Nothing fancy here, name=options[subscribe] and value="email" are added to the field to associate subscription data with email address.

If setup correctly a user should receive an email anytime a new comment on the post or page they subscribed to is merged in.

Staticman reply email notification
Example of a Staticman New reply email notification.

Well there you have it, a static-based commenting system done up in Jekyll that handles nested comments and reply notifications. Now if I could only shave a minute of my build time to get new comments merged in quicker :frowning:.

  1. An added benefit of the new configuration file means you can use Staticman with other static site generators. v2 no longer requires you to use a Jekyll specific _config.yml file. 

  2. Site properties are optional. See Staticman documentation for details on hooking up your forms

  3. 15 is not the same as '15'. Those single quotes make a world of difference… 

  4. This URL will be included in the notification email sent to subscribers, allowing them to open the page directly. 

Enjoyed this Content?

Help keep it free by sending a donation or purchasing something from my Amazon Wish List. You can also subscribe to various site feeds to get notified of new posts, follow me on social media, and more.

Related Posts


Arnab Wahid

Thanks for writing the steps in such details. I think yours is the only blog that has been laying out the details of using Staticman as the experience that followed.

Your website is one of the most, if not the most articulate example of web design I have come across, both aesthetically and technically. You are always constantly redesigning and improving this site, playing with cutting edge tech. I can only imagine what else happens before generating the site. :) How much time do you spend coding this site on average? If you do not mind me asking of course. I am asking because I am currently scared of the level of commitment that is required to treat your website as a piece of art….

This comment became so lengthy that I emailed you the rest.

Michael Rose

If you added up all the time I’ve spent on the site over the years it’s probably a :poop: ton! Writing articles definitely eats up most of the time. It takes me forever to write even the simplest post… which is why I don’t post all that often.

Articles of this length tend to take me 5–20 hours to produce over the course of a week or two. Just from all the distractions and other commitments in my life… can be hard to focus on a single thing at once :flushed:.

The design and layout stuff comes much faster. I’m constantly making small adjustments as I experiment with new things. I don’t get too hung up if I break something here, it’s one big learning experience for me.

Jared White

Wow, this looks amazing! I’m definitely going to check it out…I’ll be launching a new Jekyll-powered magazine soon and have considered comments but I definitely want to avoid Disqus if possible.

Michael Rose

How many posts do you have? Having close to 1,000 posts I’ve found they bog things down way more than installing dependencies ever could.

Found this article that introduced me to the idea of having Jekyll do less and use Gulp to do some of the heavy lifting (preprocessing Sass, concatenating/minifying JS, optimizing images, minifying HTML, etc.) instead

Was able to shave a few minutes off the build time doing just that. It complicates the build process slightly but has the advantage of not being as reliant on Jekyll — if I ever decide to switch to a different static site generator.


Thank you for this article. It’s awesome, I have successfully added static comments to jekyll. I noticed that you do not use Akismet. Is there any reason for this?

Michael Rose

I tried enabling Akismet with the following but it never quite worked:

  enabled: true
  author: "name"
  authorEmail: "email"
  authorUrl: "url"
  content: "message"
  type: "comment"

Were you able to get it working?

From this line in Staticman’s documenation I got the feeling the public instance of Staticman needed it setup. Wasn’t sure if that was the case or if it was only for those running their own instances of the app.

akismet.enabled Whether to use Akismet to check entries for spam. This requires an Akismet account to be configured in the Staticman API instance being used.

Michael Rose

Staticman recently added support for reCAPTCHA which has completely eliminated all of the spam comments that would slip through. I’m sure Akismet would have done the same, but since it’s not enabled for the public instance of Staticman this was the next best thing.

Doug Langille

Hi Michael.

Any chance you’re going to update minimal-mistakes to support the new staticman features like threading and reply notification? You’re Jekyll-fu is stronger than mine.

Michael Rose

I’d like to. Just need to give it some thought so I can come up with a way to support the new stuff in Staticman while maintaining backwards compatibility.

There are some changes needed to use v2 which could break things for those who don’t update to the new staticman.yml config.

Justin Rummel

I have a hosting provider for my site and Looking to migrate from Disqus. Is a github repo required? Can this be setup using mmistakes and not on GitHub pages?

Michael Rose

Yes a GitHub repo is required for Staticman to work. It needs collaboration access to the repo to create pull requests against it to add the comment _data files. You don’t have to host your site with GitHub Pages though.

I host with Media Temple but have my repo on GH. The workflow goes something like: new comment submitted > Staticman creates a pull request to add to site’s repo > merge pull request > re-build site > deploy.

Depending on how you deploy your site some of this may vary slightly. Previously I was building locally each time I merged in a new comment then rsync’d the contents of _site to my hosting. Now I use Travis CI to build my site after merges and it handles the deployments automatically.

Michael Rose

And yes, Minimal Mistakes supports Staticman comments. I haven’t updated it to support v2 that can handle replies and comment notifications. But that shouldn’t be too hard for you to tackle if you follow what I did in this post.

Justin Rummel

I now have Travis configured. I can display old Disqus comments, now to figure out how to post comments!

Justin Rummel

Success. I now have parent/child comments using Staticman v2 and Travis.

Last step is the Mailgun integration.

Public Test URL:

Michael Rose

Nice! FYI when I hit your site I’m getting warnings in Chrome related to SSL. Not sure if something is up with your certificate or what.

Your connection is not private

Attackers might be trying to steal your information from (for example, passwords, messages, or credit cards).

David Jones

I have been trying to update my copy of minimal mistakes with this. I have it almost working but the jquery. How do you get the function to hook into the form so it does not redirect the user?

David Jones

Thanks, I got it working with some amount of my changes plus his.

Still can’t get Mailgun to work.

Chuck Masterson

You said you got comment notifications working… I’m going to test them out here and see if I get notified. I’ve been trying to get this working on my own site and don’t understand how the two uses of options[parent] don’t conflict.

Michael Rose

My understanding is options[parent] are options[origin] are two different things, the later used to track which thread you’re currently subscribed to. But not entirely sure if that’s how it works since I haven’t had a lot of luck fully testing email notifications.

Let me know if you get a notification once this comment is merged in and posted.


Can I know when timestamp value is made in comment data file? It is hard to check it but looks important to show the comment order correctly.

Michael Rose

The timestamp is appended to the data file by Staticman when its received. You have to enable it in your config under the generatedFields array. There’s a couple of different formats you can use… I went with iso8601.


How do you apply reCaptcha for staticman? Now it is always showing “Missing reCAPTCHA API credentials” I encrypted secret using staticman.

Michael Rose

I had the same issue. Apparently there was an API change with the latest release of Staticman. In the form, changingoptions[reCaptcha][encryptedSecret] to options[reCaptcha][secret] fixed it for me.

There’s an open issue on the Staticman repo if that doesn’t end up solving it for you.


Michael, please, can you explain the page__comments snippet? If it just checks if the parent is nil, how does it display all comments? (I’m definitely missing something obvious here, but hey, it’s 1 AM). Thanks for the tutorial! One tip though: the required fields should have a required attribute. You know, for accessibility :) .

Michael Rose

Sure thing. That first assign is creating an array of comments that don’t have a replying_to field. That’s what the nil is checking since through some trial and error I learned that a replying_to field is only added to a child comment’s data file.

Inside the first loop I check the current comment’s value against all comment’s replying_to fields, if they match then they’re a child and spit out as a nested entry in the second loop.

Does that help clear things up?

Michael Rose

And good catch on the required attribute, completely missed that.


How did you apply mail notification? I already encrypted api key and domain correctly, though not having luck until now.

Michael Rose

I use the public instance of Staticman which means I didn’t have to create my own Mailgun account and encrypt keys. I didn’t have much luck getting it to work which is why I went that way.

I don’t know many people who have gotten it to work. Might be worth pinging this thread as you’re certainly not alone in trying to get it up and running.


It looks it is impossible to implement public instance of mailgun for now, sadly.

Matt Seemon

Hey Michael, I could use some help here. The below line is always assigning comments variable as blank.

{% assign comments =[page.slug] | where_exp: 'item', 'item.replying_to == blank' %}

If I do an inspect

{{[page.slug] | inspect }}

I get this

{"comment1493146261009"=>{"_id"=>"2029eee0-29e8-11e7-9eac-4b33388ae63e", "_parent"=>"http://localhost:4000/blog/karaoke/2017/04/25/Latest-tracks.html", "name"=>"Matt Seemon", "email"=>"09d1cc2c678124c06dd0ab1a0351f665", "url"=>"", "message"=>"Testing Staticman Comments", "replying_to"=>"", "hidden"=>"", "date"=>1493146261}, "comment1493150394000"=>{"_id"=>"bf915780-29f1-11e7-9eac-4b33388ae63e", "_parent"=>"http://localhost:4000/blog/karaoke/2017/04/25/Latest-tracks.html", "name"=>"Matt Seemon", "email"=>"09d1cc2c678124c06dd0ab1a0351f665", "url"=>"", "message"=>"One more test", "replying_to"=>"", "hidden"=>"", "date"=>1493150393}}

Other than that, all the settings are exactly the same, except for file name and the date format.

Michael Rose

Add an inspect on the comments array instead, since that will be the new array that is filtering out replies with where_exp.

{{ comments | inspect }}

For comparison this is an example of what I get:

[{"id"=>"comment-1047796157", "date"=>2013-09-17 07:07:59 -0400, "updated"=>2013-09-17 07:07:59 -0400, "post_id"=>"/going-static", "name"=>"picajoso", "url"=>"", "message"=>"Do you think Jekyll is suitable for a heavy updated site? If you have to build the whole website each time you publish something then this option wouldn't be acceptable. I've read somewhere that there is someway to build only the updated/new posts, maintaining the rest of the site without changes.", "avatar"=>""}]

Matt Seemon

OK.. So I sort of figured out where the problem lies… While the where_exp is not working for me as it does for you, I had to take a different approach.

{% assign idx = 0 %}

{% for comment in[page.slug] %}
    {% if comment[1].replying_to == "" %}
        {% assign idx         = idx | plus: 1 %}

        {% assign index       = idx %}
        {% assign r           = comment[1].replying_to %}
        {% assign replying_to = r | to_integer %}
        {% assign email       = comment[1].email %}
        {% assign name        = comment[1].name %}
        {% assign url         = comment[1].url %}
        {% assign date        = comment[1].date %}
        {% assign message     = comment[1].message %}
        {% include comment.html index=index replying_to=replying_to email=email name=name url=url date=date message=message %}
    {% endif %}
{% endfor %}

Michael Rose

Are you using replies on your site? You might run into problems eliminating the where filter since that first loop will include parent comments along with their children (the replies).

I had a hell of a time getting it to filter out properly. The where and where_exp filters seem to be really picky if your data array doesn’t match. It all comes down to how your data files are constructed. Perhaps yours are slightly different than mine.

Matt Seemon

Ultimately it boiled down to the fact that since where_exp was not working for me. With my approach I was receiving each comment as below. Hence the need to use [1].

["comment-1493177616768", {"_id"=>"217db4e0-2a31-11e7-9594-27fd6f0988e3", "_parent"=>"", "name"=>"Matt Seemon", "email"=>"09d1cc2c678124c06dd0ab1a0351f665", "url"=>"", "message"=>"Testing Staticman Comments", "replying_to"=>"", "hidden"=>"", "date"=>"2017-04-26T03:33:36.759Z"}]

It seems to be working with replies as well without anything breaking, so far. Fingers crossed. I will know once i go live. Thank you for all your help.



I’m really keen to try out staticman.

Is there a tool to import Blogger comments to staticman/GitHub or any suggestions on how I could go about doing so?

Thanks for sharing all this wonderful information.


Thanks a lot, Michael. I managed to get the comments imported. I first imported the comments from Blogger to Disqus, and then used your approach to export them out of Disqus.

I’m now on to configuring reply-to. I managed to get it to work but running into an issue. When running in production mode, my comments are appearing in the correct order but when running on localhost, they are order is different and the reply is showing up under the wrong parent. It seems that the order in which the comments are being returned are different on localhost and production. Any idea why that would happen?

I’m also not sure why you are not using the unique id of the parent as the reply-to value on the child to guarantee that the child is attached to the correct parent.


Michael Rose


Not sure what could be going on with the comment order. I’ve heard of others having the same issue so you’re not alone. If you’re using the same instance of Jekyll to build and serve then the output should be exactly the same. If you’re using some sort of CI service to build/deploy your site then it’s possible the environments and Jekyll versions are out of sync and that’s the issue.

I am using unique id’s to attach child comments to their parent by using the replying_to field. There’s a chance my code above is old and outdated. If you haven’t already, reference my GitHub repo instead since that is the actual code I use to build comments on my site.


I really appreciate your help.

You are right that I’m using a CI service, but after some more testing, I found that the ordering of replies is completely unpredictable and it is changing both on my local and on the production. I’m on the latest version of Jekyll (3.5). Maybe that’s causing the difference. BTW, I have been using the latest code from your GH repo.

So I made a minor modification to your code to use the parent’s _id field as the replying-to value of the child, instead of relying on the position of the comments (because that seems to be unpredictable at least in my environment). Now it seems to be working consistently.

I’m yet to get into reply notifications and MailGun setup. If I run into any issues, I may seek your help again.

I must say that your 2 blog posts on staticman have been extremely helpful. Without your posts I may not have even made a serious attempt at it.

I don’t have enough words to thank you!

Michael Rose

There’s one more thing I forgot about that I didn’t write up in the posts. If you have a page with imported Disqus comments (using the Rake task) and new ones from Staticman the order will be wrong because of filename differences.

What’s going on is the order is determined by the comment data files’ filenames. The data files created from the Disqus import use a different format (ISO 8601) than the ones Staticman generates (UNIX timestamp in milliseconds). I fixed that by renaming all of my Disqus comment filenames to match.

What you could do is modify the script to use a Unix time format (not YYYY-MM-DD) or change Staticman’s filename config to use something other than the default {@timestamp} placeholder.

If you’re looking for help with getting Mailgun notifications working I’d check out Staticman’s repo. I’m using the public instance so no issues really for me, but I know a lot of people have had problems getting it to work. You’ll likely find more guidance on this issue’s thread than from me.


Thanks, Michael.

You are right that Disqus comments and staticman comments have different file naming conventions, but I haven’t encountered any issue with that. I use the date inside each comment file to sort the comments in the right order. So, file name doesn’t seem to matter much.

I do have 2 questions for you -

  1. I had recaptcha enabled for my staticman comments, but Page Speed checks do not seem to like recaptcha for various reasons (no browser caching etc.). So I experimented with disable recaptcha and falling back on the honey pot method. Within 6 hours, I received 4 spam comments. This tells me that honey pot is not as effective as recaptcha in dealing with spam. However, Eduardo Boucas himself is not using recaptcha on his blog, and he has commented on the issue thread, which you linked in your reply, that honey pot has worked well for him. So, I wanted to know your experience with it.

  2. Comment replies - I’m concerned about using comment replies because I’m wondering what if I put someone else’s email address and subscribe to comment replies. Wouldn’t that start spamming the mailbox of the person who owns the email address? Is there some kind of email verification involved when you subscribe to replies? I don’t seem to recollect there was one when I signed up for replies on this thread.

Thanks again.

Michael Rose

I wouldn’t worry too much about the script getting flagged by page speed testers — the scores they assign aren’t as black and white as they make it seem. It’s being loaded asynchronously and I haven’t seen it hurt my pages much if any.

The honeypot method is too easy for bots to spoof. They eventually learn what the hidden field is that you’re trying to fool them with. I was getting over 10 spam comments a day on my posts. Turning on reCAPTCHA I get maybe 2 a week. Spam that slips through is usually people manually trying to stuff a comment with their links, on posts like this that tend to rank well in search.

Reply notifications are totally something that could be abused. You’re right someone could easily put any email address they wanted into the form. I believe I saw someone over in Staticman’s issues on GitHub voiced the same concern and suggesting some sort of double opt’in feature.

Regardless I believe the notification emails that come from Staticman have an unsubscribe link, so it should be easy enough to stop them.


Thanks, Michael. I’m going to first re-enable Recaptcha as I don’t think the honey pot is sweet enough. About reply notification, I guess you are right that the unsubscribe option should mitigate the risk. I’ll give it a shot and see how it goes.

One other thought I had was to make use of browser local storage to store commenter’s Name, Email and Website so that return visitors who have commented before do not have to type those details again (as long as they don’t clear their cache). What do you think about that?

Gonzalo Ziadi

Michael, thanks a ton for this post! I used it as the base for my own implementation over at Some things I had to change:

  • “item.replying_to == ‘’ “ rather than “item.replying_to == blank”
  • {% assign comments =[page.slug] | where_exp:”item”, “item.replying_to == ‘’” | sort: ‘date’ %} rather than {% assign comments =[page.slug] | where_exp:”item”, “item.replying_to == blank” %} as even though I followed your comment naming convention, posts were being sorted in reverse alphabetical order so the replies were showing under the wrong parent comment. Now that I think about it maybe | reverse would work the same as | sort ‘date’ in my case. I also tried to use the comment _id field as the “link” between child and parent rather than the for loop index, but I kept getting errors about ‘nesting too deep’. I feel like using the _id field would be better though as then sorting the comments by date or some other parameter would become doable. What do you think?
  • I didn’t want to use jquery so I changed a bit how the replies work. Same concept, different implementation.
  • I changed the css a little, but it is heavily inspired by yours.
  • I changed the website input type to text instead of url. The url regex is annoying, to me, as it requires users to put the http:// in front of their url. I feel like they shouldn’t have to do this.

If it piques your curiosity in the very least, in my _includes folder I have your same three files (comments, comment, and comment-form) and in my _layouts folder in default.html in a script tag at the bottom of the file I have the js code for the replies. Thanks again!

Michael Rose

Thanks for sharing! I’ve heard from a few people that they had to modify the for loop to get the nested order sorted out. Something not mentioned in this post is I did some additional cleanup on my comment data (see this reply).

Some of the variation might have to do with your comment data too and how Liquid conditionals are met. Truthy/falsy values for strings, integers, arrays, etc. are different.

I encountered the same nesting too deep errors as I tried a million different combinations of Jekyll’s where and where_exp filters. There have been some updates Jekyll so maybe it’s worth revisiting.


Many thanks for this article. After Pooleapp dead i thought i was sentenced to Disqus again. Staticman to the rescue !!

Comments are closed. If you have a question concerning the content of this page, please feel free to contact me.