Staticman: An Alternative to Disqus for Comments on Static Sites

Staticman: An Alternative to Disqus for Comments on Static Sites
Figure: staticman.net

Comments are an important aspect of many websites, particularly blogs, whose success depends on their ability to create communities. However, including comments is inherently more difficult for static websites than for dynamic websites (e.g. managed through Wordpress). With Hugo, comments can be easily integrated via Disqus. The disadvantage, however, is that foreign JavaScript code needs to be executed and that the comments are not part of the page itself. Here, I will explain how comments can be integrated into a web page using Staticman.

What is Staticman?

Staticman is an API, which allows for the integration of comments into static web pages. What is the idea of this approach?

  • To create a new comment, a request is sent to the Staticman API.
  • If the moderation option is set to true, a Staticman GitHub account will automatically create a pull request for the submitted comment. The user can then decide whether to merge the pull request or close it. If the moderation option is set to false, comments are automatically merged into the page.
  • The static web page is rebuilt and the comments are displayed.

Problems with the Staticman API

Unfortunately, it seems that the public Staticman API is overstrained. When I wanted to use the API, the Staticman GitHub account would not accept my collaboration invitation quoting “Invitation not found” when trying to connect to the API as described in the official documentation. It turned out that many other people also had the same problem. Luckily, the problem can be overcome by hosting your own version of the Staticman API. So this is what I did.

Hosting your own Staticman API

To host my own version of the Staticman API, I’ve followed this great tutorial. Here, I’ll briefly summarize the required steps for getting Staticman running for a GitHub repository.

  1. Create a new GitHub account for the Staticman API
  2. Create a GitHub token for this account, allowing API/write access
  3. Clone an instance of Staticman
  4. In the staticman folder, create a file called Procfile and store a single line in there: npm start
  5. Create a private RSA key for use with the API: openssl genrsa -out key.pem
  6. Create an account at Heroku, a free web app hoster and download the Heroku CLI
  7. After logging into the CLI using heroku login, create a new Staticman app and configure it: cd ~/staticman heroku create {nameOfYourStaticmanApp} $heroku config:set NODE_ENV="production" $heroku config:set RSA_PRIVATE_KEY="$(cat key.pem)" $heroku config:set GITHUB_TOKEN="Your_Token"
  8. Create a production branch: git checkout -b production origin/dev
  9. Add config.production.json to .gitignore
  10. Commit the changes: git add config.production.json Procfile .gitignore git commit -m "Set up Staticman v3 for deployment to Heroku"
  11. Deploy the API: git push heroku production:master
  12. After the API has been built successfully, you should be greeted with Hello from Staticman version 3.0.0! when visiting your API instance at https://{nameOfYourStaticmanApp}.herokuapp.com.

Limitations of Heroku apps

Free Heroku apps come with some limitations. The most important limitation is that the number of free dyno hours is limited. A dyno is the isolated container in which your application is running. A free dyno begins to sleep after 30 mins of inactivity. Otherwise, it is always on as long as you still have remaining dyno hours (currently 550 free dyno hours per month).

When your free dyno hours are exhausted, all of your free dynos start sleeping. You will receive a notification from Heroku before this happens though. If the only app that you are running is Staticman, however, it is unlikely that you will use up you free dyno hours because this would mean that you are receiving comments for more than 18 hours per day, which would be quite a lot.

So, the main disadvantage of free dynos when is that posting comments will be slow (delay of a few seconds) when no one has posted anything within a while (30 minutes). In my opinion, this limitation is sufferable for a free service. If you want to get rid of this limitation or if you need more dyno hours, you can always upgrade to a paid dyno.

Reconfiguring your theme

Once the API is running, you need to adjust your Hugo theme to let users submit comments through the API and have the available comments in your data folder displayed. There are already several tutorials available that deal with this problem, for example, here and here.

Since I had some problems with these tutorial, I’d recommend starting with the commenting system from the official demo site. I’ll shortly go through the most important aspects:

  • The configuration of the API is done via staticman.yml. Adjust these values according to your needs.
  • Adjust the CSS for the comments to your needs.
  • Adjust the template responsible for comments in your theme folder using the templates from the demo.

When I tried to get the comments template working, I couldn’t get any comments displayed because the unique IDs never matched. Thus, I’ve ended up with a modified partial template for the Staticman commenting system. The idea of my approach is to store the path of the page in the comment file and then to check whether the comment’s path is a subset of the page’s path via {{ if in .path $.File.Path }}.

Adding a comment counter

A nice addition to the system would be to show the number of comments associated with every post in the list view. This can be easily achieved by using the hasComments variable defined in the commenting template. We simply create the following partial template for the comment counter, which displays a representative SVG as well as the count using hasComments.

<div class="meta__item-comments meta__item">
    <svg class="meta__icon icon icon-comments" width="20" height="20" viewBox="0 0 20 20">
    <path d="M14.999,8.543c0,0.229-0.188,0.417-0.416,0.417H5.417C5.187,8.959,5,8.772,5,8.543s0.188-0.417,0.417-0.417h9.167C14.812,8.126,14.999,8.314,14.999,8.543 M12.037,10.213H5.417C5.187,10.213,5,10.4,5,10.63c0,0.229,0.188,0.416,0.417,0.416h6.621c0.229,0,0.416-0.188,0.416-0.416C12.453,10.4,12.266,10.213,12.037,10.213 M14.583,6.046H5.417C5.187,6.046,5,6.233,5,6.463c0,0.229,0.188,0.417,0.417,0.417h9.167c0.229,0,0.416-0.188,0.416-0.417C14.999,6.233,14.812,6.046,14.583,6.046 M17.916,3.542v10c0,0.229-0.188,0.417-0.417,0.417H9.373l-2.829,2.796c-0.117,0.116-0.71,0.297-0.71-0.296v-2.5H2.5c-0.229,0-0.417-0.188-0.417-0.417v-10c0-0.229,0.188-0.417,0.417-0.417h15C17.729,3.126,17.916,3.313,17.916,3.542 M17.083,3.959H2.917v9.167H6.25c0.229,0,0.417,0.187,0.417,0.416v1.919l2.242-2.215c0.079-0.077,0.184-0.12,0.294-0.12h7.881V3.959z"></path></svg>
    {{ $nbrOfComments := 0 }}
    {{- if ( $.Scratch.Get "hasComments") }}
        {{ $nbrOfComments = $.Scratch.Get "hasComments" }}
    {{- end }}
    <span class="meta__text">{{ $nbrOfComments }}</span>
</div>

Then, we simply include the created file in the template that displays the post meta information with the following command:

{{- partial "post_meta/comments.html" $root -}}

Remaining issues

The only remaining issue I have is that it doesn’t seem to be possible to load comments from subfolders of data/comments. It seems that Hugo doesn’t allow access to files when looping over diffferent folders because file access seems to require specific paths rather than dynamic paths. For example, you can access the comment files using .Site.Data.comments but you wouldn’t be able to access files by constructing a path via print .Site.Data.comments <pathToDirectory>.

Thus, all comments of a site have to be stored in a single folder, which could be a problem if there are thousands of comments. For small sites, however, this should be fine.

I hope this overview helped you getting started with Staticman for Hugo. Many thanks to Eduardo Bouças for this great tool! If you have any questions, just write a comment below ;-)

Author: Matthias Döring