Adding comments to a Jekyll site with Staticman
Offloading comments to an third party service like Disqus1 has always felt like a necessary evil to me when building static Jekyll sites.
Convenient to embed a small bit of <script>
voodoo from them into your pages — sure. But kiss goodbye to controlling the user experience, look and feel, site performance, data, and your privacy. Disqus alternatives haven’t been all that great for the statically-minded unless you were willing to make some compromises…
In this tutorial follow my journey as I explore the possible ways of adding commenting to a Jekyll static site. Eventually working towards a setup that could statically generate comments from files not at all different from those already consumed by Jekyll for posts and pages.
Self hosted comment systems
Isso describes itself as “commenting software similar to Disqus.” You host a SQLite database and embed some JavaScript on your pages (just like Disqus and friends) and you’re ready to roll. After freeing my content from Wordpress and “going static” I really didn’t want to manage a server and database again just to have comments on my site. So these solutions were out.
Other self-hosted commenting systems include: Commento, Remark42, Discourse, talkatv, Juvia, HashOver, and Savas.
GitHub based comments
If you’re against using a 3rd party or spinning up your own server. The thought of leveraging GitHub’s API for commenting may appeal to you. utterances and giscus are two open source projects that rely on GitHub issues and discussions to store comments.
Connect their respective app to your GitHub repository, configure how posts are mapped to issues/discussions, set a theme, add lightweight JavaScript to your site, and you’re done.
Both are ideal solutions for sites with an audience that skews towards developers and may already have a GitHub account. I want something platform agnostic and didn’t require any sort of account to leave a comment… so the search continued.
Static comments
What I really was searching for was a commenting system to compliment the rest of my statically generated Jekyll site.
Over the years I’ve come across several solutions that seemed promising:
- Use
mailto
links (how very retro) to email comments to yourself to then manipulate and add directly to a post. - Use PHP to do something similar.
Jekyll plugins like Jekyll::StaticComments and Jekyll AWS Comments were pretty close to what I was looking for. A PHP <form>
captures a comment, converts into YAML and emails it over to be placed in a prescribed location. Then with the help of a Liquid for
loop, comments are displayed on the appropriate pages.
I can’t really explain it, but something about using PHP in combination with Jekyll felt off to me. And so I didn’t pull the trigger on these solutions either.
It wasn’t until I discovered Eduardo Bouças’s blog post “Rethinking the Comment System for My Jekyll Site” and the launch of Staticman 2.0 that I finally decided to remove Disqus from my site.
And with that, started the process of migrating years of comment data and integrating them into the rest of my statically built site.
Enter Staticman
On paper and in practice Staticman was the app I was looking for to power static-based commenting on my site.
- Designed to work with Jekyll and GitHub Pages.
- Free and open source. Run it on your own server as a Node.js app or go the free hosted route with Heroku.
- Complete control over the data, content, user experience, and user interface.
- Not just for comments! Perfect for any sort of user generated content: reviews, comments, polls, and more.
- User submitted content can be merged in automatically or moderated.
Getting started
Much like building my first Jekyll site, I found the process of integrating Staticman into my workflow very rewarding. It was nice to get dirty again crafting form markup and styling comments to fit in with the rest of my site.
Thankfully I didn’t have to start from scratch as I was able to draw inspiration from the Staticman demo site — Popcorn and Eduardo Bouças’s personal site. The documentation for Staticman does a good job of explaining how to set things up so definitely give that a read first to familiarize yourself with what the app can do.
Building the form
I set my gaze on marking up the “Leave a comment” submission form first. Seemed like an easy target as the styling of various form elements like <input>
, <label>
, <textarea>
and buttons were already done as part of my living style guide.
To complete it, was to make a decision on what fields I wanted to capture, and write a little bit of JavaScript for form handling and submission. Arriving at this for my post__comments.html
include (class
names and Liquid removed for brevity).
<form id="comment-form" method="post" action="{{ site.staticman.endpoint }}{{ site.repository }}/{{ site.staticman.branch }}">
<fieldset>
<label for="comment-form-message">Comment</label>
<textarea type="text" rows="3" id="comment-form-message" name="fields[message]"></textarea>
</fieldset>
<fieldset>
<label for="comment-form-name">Name</label>
<input type="text" id="comment-form-name" name="fields[name]"/>
</fieldset>
<fieldset>
<label for="comment-form-email">Email address</label>
<input type="email" id="comment-form-email" name="fields[email]"/>
</fieldset>
<fieldset>
<label for="comment-form-url">Website</label>
<input type="url" id="comment-form-url" name="fields[url]"/>
</fieldset>
<fieldset class="hidden" style="display: none;">
<!-- used by Staticman to generate filenames for each comment -->
<input type="hidden" name="options[slug]" value="{{ page.slug }}">
<!-- honey pot used to filter out spam -->
<label for="comment-form-location">Not used. Leave blank if you are a human.</label>
<input type="text" id="comment-form-location" name="fields[hidden]" autocomplete="off"/>
</fieldset>
<fieldset>
<button type="submit" id="comment-form-submit">Submit Comment</button>
</fieldset>
</form>
<!-- End new comment form -->
Staticman’s documentation covers this in more detail, but essentially I’m adding fields[]
values to the name
attributes. message
, name
, email
, and url
fields are then used to generate a .yml
file similar to this:
message: "![Bill Murray](http://www.fillmurray.com/400/300)\r\n\r\n“It's hard to be an artist. It's hard to be anything. It's hard to be.”"
name: Bill Murray
email: b0caa2a71f5066b3d90711c224578c21
website: ''
hidden: ''
date: '2016-08-11T19:33:25.928Z'
Some notes on the hidden
and date
fields you may have noticed in the sample comment above:
Hidden: is used as a spam deterrent in the form of a honeypot. The thought is a human wouldn’t fill out an input they can’t see, but a spam bot may. Adding fields[hidden]
to this input and not placing it in the allowedFields
array in our Jekyll config, instructs Staticman to reject the entry. Hopefully filtering out bots who are dumb enough to populate it with something.
Date: is captured when the entry is generated by Staticman. Its format can be changed from iso8601
(default) to timestamp-seconds
or timestamp-milliseconds
.
Interactions and state
Using Popcorn’s main.js
as a guide I added all the AJAX goodness, alert messaging, along with disabled
and loading form states.
To avoid disrupting the flow too much I went with inline alert messaging directly above the submit button.
And to improve the user experience upon submission the submit button’s text changes to Loading...
, becomes disabled, and an animated SVG icon inserted for bit of extra flare.
$(form).addClass('disabled');
$('#comment-form-submit').html('<svg class="icon spin"><use xlink:href="#icon-loading"></use></svg> Loading...');
If the form is successfully submitted a message appears notifying the user that the comment has been received and is pending moderation. Since my site takes a bit to generate with Jekyll I felt it necessary to convey this to the user, hopefully avoiding duplicate submissions.
With smaller sites hosted with GitHub Pages this becomes less of a problem — as they build much faster. Especially true if you decide to go with the auto merge option and skip moderating comments.
Displaying comments
There’s a bunch of Staticman settings available to you, but forget all that for right now. The important bit to remember is: static comment files will live in _data/comments/<post slug>/
. By predictably placing them here we will be able to access their contents from the following array: site.data.comments[page.slug]
.
With this array we’ll be looping through it with for
just like you would with site.posts
to spit out a list of all posts. But first we’ll use an assign
tag to rename the array and apply a sort
2 filter on the objects. This will order them by filename, which in our case should be chronological3.
{% assign comments = site.data.comments[page.slug] | sort %}
{% for comment in comments %}
show a comment
{% endfor %}
Since I’m capturing message
, name
, email
, and url
in the comment form these will be the same fields I’ll want to pull from to build each comment. Using an assign
tag again we’ll cleanup variable names like comment[1].avatar
into just avatar
. Which will then be used to pass parameters into the comment.html
include:
{% assign comments = site.data.comments[page.slug] | sort %}
{% 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 index=forloop.index avatar=avatar email=email name=name url=url date=date message=message %}
{% endfor %}
If done correctly the values and strings in a data file like _data/comments/basics/comment-2014-02-10-040840.yml
---
id: comment-1237690364
date: '2014-02-10 04:08:40 +0000'
updated: '2014-02-10 04:08:40 +0000'
post_id: "/basics"
name: Tamara
website: ''
message: "This? This is freakin' awesome! Thanks so much for sharing your mad skills and expertise with us!"
Should pass through _includes/comment.html
and spit out as the following HTML:
<article id="comment1" class="js-comment comment" itemprop="comment" itemscope itemtype="http://schema.org/Comment">
<div class="comment__avatar-wrapper">
<img class="comment__avatar" src="https://www.gravatar.com/avatar/?d=mm&s=50" srcset="https://www.gravatar.com/avatar/?d=mm&s=100 2x" alt="Tamara" height="50" width="50">
</div>
<div class="comment__content-wrapper">
<h3 class="comment__author" itemprop="author" itemscope itemtype="http://schema.org/Person">
<span itemprop="name">Tamara</span>
</h3>
<div class="comment__date">
<a href="#comment1" itemprop="url">
<time datetime="2014-02-09T23:08:40-05:00" itemprop="datePublished">February 09, 2014 at 11:08 PM</time>
</a>
</div>
<div itemprop="text"><p>This? This is freakin’ awesome! Thanks so much for sharing your mad skills and expertise with us!</p></div>
</div>
</article>
Looking like this when styled with CSS
:
There’s not much magic in the comment.html
include — some structured data markup sprinkled about and a few Liquid conditionals for displaying author avatars and URLs.
Setting up Staticman
With the front-end portion of my static-based comment system squared away, it was time to configure Staticman. Because I went with the hosted version, it only took a few quick steps to setup.
Adding Staticman as a collaborator
First you need to grant Staticman access to your Jekyll repository on GitHub. You don’t have to actually host the site there (I use Netlify for that), but it does need to be a standard Jekyll site with valid _config.yml
.
Following the docs I added GitHub username staticmanapp
as a collaborator and then pinged https://api.staticman.net/v1/connect/{your GitHub username}/{your repository name}
as instructed to accept the invitation.
Configuring Staticman
Staticman is configured by settings defined in your Jekyll _config.yml
under a staticman
object. There’s a whole list of stuff you can configure — the important stuff being allowedFields
, branch
, format
, moderation
, and path
.
There’s also an undocumented generatedFields
4 setting that is useful for time stamping each file Staticman creates.
I ended up with the following settings in my _config.yml
:
staticman:
allowedFields : ['name', 'email', 'url', 'message']
branch : "master"
commitMessage : "New comment."
filename : comment-{@timestamp}
format : "yml"
moderation : true
path : "_data/comments/{options.slug}"
requiredFields : ['name', 'email', 'message']
transforms:
email : "md5"
generatedFields:
date:
type : "date"
options:
format : "iso8601"
In case any spam makes it through, you may like another layer of “protection” to block it. Setting moderation: true
will make Staticman send a pull request whenever a new comment entry is submitted. At this point you can examine the content inside of the PR and decide if you want to merge or close it.
When hosting with Netlify, GitHub Pages, and the like — a merge will instantly force Jekyll to rebuild the site and deploy. Since I self host I have the extra step of pulling from remote
, before building locally and deploying via rsync.
Hooking up the form
For your forms to work with Staticman they need to POST
to:
{Staticman endpoint}{your GitHub repository}/{your repository name}/{the name of the branch}
Instead of hard-coding these values, use site
variables defined in the _config.yml
file. For example {{ site.repository }}
and {{ site.staticman.branch }}
respectively.
# sample _config.yml
repository: "mmistakes/made-mistakes-jekyll"
staticman:
branch: "master"
Hitting the Staticman endpoint should trigger the success and error messages in our comment <form>
. Firing up the console in your browser of choice can also give you some more hints on what’s going on if you encounter any snags.
For example if all of the required fields aren’t filled out an error like this could hit the console:
Object {readyState: 4, responseText: "[{"code":"MISSING_REQUIRED_FIELDS","data":["name","email","message"]}]", responseJSON: Array[1], status: 500, statusText: "error"}
Publishing comments
If configured correctly you should receive a pull request notification on GitHub each time a comment entry is submitted. Look the commit over (if you’re moderating them) and merge pull request to accept or close to block it.
Migrating Disqus comments
It was now time to deal with the 500+ Disqus comments I’ve accumulated. A good chunk of them had valuable content worth keeping, so I didn’t want to ditch them all.
I came across a Rake task by Patrick Hawks, aptly named jekyll-disqus-comments that downloads Disqus posts as YAML files via the Disqus API.
With some modifications I was able to get it working with my Jekyll site and posts files.
Installing
Copy the following files to the root of your Jekyll project folder.
_rake/disqus_comments.rake
Rakefile
(Not necessary if you already have a Rakefile that loads_rake/*
)
Obtain a Disqus API public key
To use the plugin, you will need to acquire a public key from the Disqus API and add it to your _config.yml
. You can do this by:
Step 1. Register new application.
Step 2. Setup application using suggested configuration below:
Label: <Name of application> eg. Jekyll Disqus importer
Description: Convert comments into static files.
Website:
Domains: disqus.com
Default Access: Read only
Step 3. Add the following lines to your _config.yml
:
comments:
disqus:
short_name: YOUR-DISQUS-FORUM-SHORTNAME-HERE
api_key: YOUR-DISQUS-PUBLIC-KEY-HERE
Run import task
Import comments from Disqus by running rake disquscomments
from the CLI. If it completes without error you should find a set of .yml
files in _data/comments/<post-slug>/
similar to this:
├── _data
| └── comments
| └── 365-days-of-drawing
| | └── comment-2013-08-30-162902.yml
| | └── comment-2013-08-30-204505.yml
| └── basics
| └── comment-2014-02-10-040840.yml
With each of these files should having front matter data similar to this:
---
id: comment-1237690364
date: '2014-02-10 04:08:40 +0000'
updated: '2014-02-10 04:08:40 +0000'
post_id: "/basics"
name: Tamara
website: ''
message: "This? This is freakin' awesome! Thanks so much for sharing your mad skills and expertise with us!"
Key names correlate with the ones defined earlier with Staticman, along with a few specific to Disqus: id
, updated
, and post_id
that aren’t currently used on the site.
I’m a little obsessive so I went through a ton of old comments adding GitHub Flavored Markdown5 backticks to improve the reading experience of code blocks. Having properly formatted code blocks in comments looks so good I couldn’t pass it up.
Pulling this off with Disqus required way more work and didn’t support Markdown.
Troubleshooting
When running rake disquscomments
I ran into warnings like this:
Comments feed not found: <domain.com>/post-slug/
For posts that I knew didn’t have any comments this wasn’t a problem, but for those that did it was a real head scratcher. Eventually I discovered that ident
in disqus_comments.rake
wasn’t matching the style of post paths used on my site.
I was able to determine what Disqus was expecting for id’s and adjust the plugin by:
- Exporting all my Disqus comments as XML.
- Opening the Disqus XML file.
- Looking at the
<link>
elements eg.<link>https://mademistakes.com/mastering-paper/contour-drawing/</link>
By playing around with the following line in disqus_comments.rake
I finally sorted it out:
# site.url + post.id + trailing slash
ident = site['url'] + post.id + '/'
Final thoughts
Treating comments as content and integrating them into the same build process as the rest of my site has been an informative and rewarding experience. By successfully migrating over 500 comments away from Disqus I was able to:
- Style them consistently to match the rest of the site’s design.
- Improve the appearance of
<code>
blocks within comments. - Make it easier for visitors to leave a comment without having to create a Disqus account.
SEO implications
The comments left on many of my posts often contain corrections, follow-up, and other valuable post content. From earlier tests it did seem as if search engines were able to crawl the embedded Disqus JavaScript comments and partially index them. Time will tell if I’ll see any SEO lift now that comments are part of the HTML and marked up as structured data.
Spam slipping through
Seems to only happen on my older posts or ones that rank well with Google and friends. As no one is really adding valuable comments to these I’ve added a comments_locked
conditional to disable the comment form on specific pages.
{% unless page.comments_locked == true %}
<!-- comment form -->
{% else %}
<p><!-- comments locked messaging --></p>
{% endunless %}
I’ll have to keep an eye on the effectiveness of this method, or possibly find a tastier honeypot to better combat spam bots.
Comment replies
One thing I miss since leaving Disqus, are comment notifications. Sure you can setup GitHub to notify you of each Staticman pull request, which will in turn clue you in that you have a new comment. What’s missing is a way to notify the commenter that there’s been a reply to their comment.
Less likely a commenter will return to the page to see if a reply was made without the nudge of a notification. Wordpress and friends has the whole “subscribe to comments” feature which could apply here I suppose.
There are several third-party commenting services to choose from: Disqus, IntenseDebate, Facebook, and countless others. They all essentially work the same — you embed someone else’s JavaScript on your site and comments magically appear. ↩︎
Sort an array. Optional arguments for hashes: 1. property name 2. nils order (first or last). ↩︎
eg.
comment-2014-02-10-040840.yml
,comment-2015-03-22-204128.yml
, etc. ↩︎Adds a
date
timestamp to entries in ISO8601, seconds, or milliseconds formats. ↩︎The
markdownify
filter is used in_includes/comment.html
to convert Markdown-formatted strings found in{{ include.message }}
into HTML. ↩︎
31 comments
This post definitively deserves static comments.
Thank you once again to rise the bar for the development of Jekyll themes. Now I want to try this, too. Disqus was – like you wrote – a necessary evil. Staticman looks like the ideal solution.
It’s pretty darn slick that’s for sure! Hardest part to the whole thing was getting all the Disqus comments converted into
.yml
files.This is simply fantastic, Michael. Great work! I’m curious to know how the switch to static comments has affected your crawl stats in Google Search Console. If I had to guess you should see more frequent crawling and less time spent downloading, which could carry aggregate benefits not obvious until analyzed over a longer period of time.
Since you’re looking for alternative static commenting strategies…I stumbled upon jekyll-aws-comments today linked from lambda-comments. It’s probably more complex to get going but ideal for control freaks who don’t want to rely on a 3rd party, and who might already be hosting their Jekyll blogs on AWS with CloudFront for example. Again, great work! Love it!!
Thanks Josh. I came across those as well in my search but since I don’t have any experience with AWS I passed them over. Definitely seem like good options though, especially if you’re hosting with AWS.
As far as stats. I launched static comments in mid-August, looking at my crawl stats there isn’t much that jumps out to me. Kilobytes downloaded seems to be trending up but hard to tell.
One thing I’ve noticed is the need for better comment spam filtering. I get around 1-3 submitted comments a day that are clearly spammers trying to get backlinks. Back with Disqus I got zero, so it must have been doing a good job of filtering that junk out.
Not entirely sure if its bot driven or not but thinking if Staticman adds support for ReCaptcha, that might help combat it.
I have been searching for a decent alternative of Disqus for a few years now. This is the best one so far. Thanks a lot, Michael!
I was wondering, does Staticman support threaded comments? If not, would I be able to get it by tinkering with the styling?
@Arnab - I’m sure Staticman could be “bended” to support threaded comments, but it would take some work on your end. Off the top of my head I’d probably do something similar to how Wordpress adds reply links to each comment.
That could be used to create a variable that references the comment # being replied to. Which you’d send with the rest of the comment data to Staticman, adding it the YAML Front Matter.
Then in the comment Liquid code you’d add some sort of check for that variable and if present on the current comment, pull in any replies. Probably using something like the
where
filter.Hey Michael, Great post indeed, However I have some questions:
With thanks
Since Markdown is supported you can write something like:
To add images to a comment… they need to be hosted somewhere though since as far as I know Staticman doesn’t deal with uploaded files.
Also. I’m pretty sure
slug
is available to both posts and pages. I know for certain it works with collection documents because I use this exact same method to add comments to my FAQ section, which is built using a collection.Experiment and try it out. As long as a page has a unique identifier you should be able to pull comment content from a
_data
file.Thank you Michael. As far as I tried, there was no page slug available for pages. I solved this problem by defining
slug
as a custom variable for each page.Thanks Michael. I have a question: how can I add Staticman comments to my blog? I’m using Windows and host my Jekyll on Firebase.
@Duc - Start by reading this post. The steps I followed to add Staticman are all here. I have no experience with Firebase so I’d be no help there but the Jekyll side of things would be very similar to what I’ve outlined above.
I am revamping my website and moving to Jekyll. I am not sure, if Staticman works in case I am pushing only
_site
folder to Github?@Bozdar - I think it’s doable, just might introduce some complexity to your build/deploy process.
For example, I don’t use GitHub to build or host my site, but simply store my source files there in a repo. My build/deploy process with Staticman goes like this:
_site
folder to my server. I use a Gulp rsync task but there are several other options available to you.All Staticman needs is to be made a collaborator on your repo and be able to see your
_config.yml
so it can be configured."Michael. Then, it seems a bit easy but real situation is that I am hosting my website on Github, at least for the time being. So, I do not find Staticman helpful enough because it needs
_config.yml
file to work. I may use Staticman in future when I change my host.Many thanks, for the help.
Thanks for the awesome theme Minimal Mistakes.
Unfortunately, I couldn’t configure static comments for my project.
I receive pull requests on github, but comments not published in my post. https://github.com/danykeep/danykeep.github.io
Help me please, how can i solve this problem? I tried to turn to false moderation option, but it didn’t help.
@Dany - Change Staticman’s path in
_config.yml
. You were probably using the demo site’s path which places the comment data files in a different location. You want it set as:Move your existing
/comments/
folder under/_data/
after you make the config change and on rebuild they should up.Staticman now supports threaded comments. The Liquid you need to craft can get messy, but it’s manageable if you don’t nest too deep.
Thanks for the update. I have been meaning to bother you with a couple of questions but been hesitant about it for obvious reasons. I am still not sure if I should attempt a conversation that will probably never end! However, initially , I am just looking for some advice. But first, let me read your latest post first. I will continue my nervous babbling there.
This code has been a terrific help to me for a site I’m working on… but I’m having quite a time trying to sort my comments by the date.
{% assign comments = site.data.comments[page.slug] | sort: "date" %}
is throwing an error to the effect that there’s no implicit conversion of String into Integer. My filenames aren’t going to be guaranteed sequential, so sorting by those won’t work… any idea what might be going wrong here?I had the same problem when trying to sort using the
date
value. I think the issue is Staticman captures that field as a string since it’s encased in single quotes, causing Liquid to throw an error as it can’t compare strings against integers… or a date timestamp.I tried everything I could think of to convert the
date
values from a string so they could be sorted, but never found a solution. Jekyll has several filters for converting to various date formats and even theto_integer
, so I thought if I could capture the array, filter it the date fields, then I’d have an array with date values that I could sort. Never got that working though.In the end I threw in the towel and just renamed all my legacy comment
_data
files so they were sequential using a Unix timestamp, matching the same filename format I defined for Staticman (filename: comment-{@timestamp}
).I had similar problem with @Dany, and it has been resolved by changing
path
and/comment/
folder.But, now I get another problem that comment cannot be submitted. Error message:
Thank you. Repo: https://github.com/nurandi/nurandi.github.io
@Nur - Likely something is off with your Staticman config. If you open your browser’s web development tools and look at the Console output it will give you a better idea of what data from the form Staticman doesn’t like.
For example if I try to submit a comment with the name and email fields blank you’ll see this error in the Console.
@Michael - Problem has been resolved by changing:
To:
@Michael, I get problem when import comment from disqus by running
rake disquscomments
Any idea what might be going wrong here?
The error message is a clue. It’s looking for the domainmatrix gem. You likely don’t have it installed so it’s failing. Try
gem install domainmatrix
On an individual post, how can show comments but disable any new comments (maybe by not showing the ‘add comment’ box)? Am using staticman.
You can do something like what I’ve done wrapping the comment form with a Liquid conditional:
Then I add
comments_locked: true
to a post’s YAML Front Matter that I want to disable adding new comments to. You can see an example of how that looks here.Thank you for this great article. I will have a look at how to use this in Hugo (I am just starting with Hugo but it should not be that of a big deal with the details you provided)
Thanks for the post. I’m looking to add comments to my blog…though I can’t help but wonder, if my website is to be interactive, why aren’t I using wordpress or something? Feels like a lot of trouble to do something that should be pretty basic