Hugo with Staticman Commenting and Subscriptions

Continuing on from the last post, in which we set-up the BinaryMist blog and business site on Hugo, it was now time to provide some functionality for you to be able to:

  1. Leave comments on posts
  2. Subscribe to comments on specific blog posts
  3. Subscribe to notification by email as new blog posts are published

Hugo uses Disqus as it’s default reader commenting system which is a shame, as Disqus is evil, but never fear, I’ve gone ahead and got a commenting system set-up for the Academic theme that won’t abuse you. Your comments are actually hosted from the same place as this website is running from, your email address is md5 hashed, so it’s not visible to the public in clear text anywhere. For example, this is the hash of my email address:

What’s on Offer?

I reviewed the other offerings promoted by Hugo:

  • Disqus: As discussed, No thanks!
  • Texpen: Their site doesn’t respond for me
  • IntenseDebate: Closed source (I think). Looks like they want to sign the consumer up, I’m guessing this costs money. What ever their deal is, they’re not up-front enough about it for me
  • GraphComment: Closed source
  • Mutt: Closed source, costs money
  • isso: Python and open (great), but the consumer has to self host, may not be a show stopper, but we can do better

This leaves the free and open Staticman. You can see some of the other sites that use Staticman here. Staticman ticked all of my boxes, apart from the fact I’d like to see some more contributors to the project, but hay, if that is honestly a problem, then I should be contributing, and I guess I am in a small way. In saying that, Worst case scenario, is that the project becomes dormant, and one of the consumers, will need to become a producer, and/or we have to self host it. Big ups to @eduardoboucas for running this project for free, and in his spare time. It’s people like this that make the open source world what it is.

Staticman it is, Let’s move on

If you subscribe to comments on any of the BinaryMist blog posts, your email address will be stored in a mailing list that I control. You can unsubscribe at any time, and I will not spam you. You can also tell me to remove you at any time and I will make sure your request is honoured. If you check the “Notify me of new comments on this post” (on this or any other post), your email will go into my private mailing list, and will also only be visible as a hash in a Github pull request. That’s right, they look like this (01486cfc6aa638a6f8e85142c645fcd7) remember? You can also see what these look like here.

Now that I had decided to go the Staticman route, I needed to consolidate on the documentation, examples, and start to build a picture of how this was going to work.

If I had to do this again I would ignore both of the above Beautifulhugo and gohugohq examples.

The Staticman official documentation is good, but will be even better with a little more time, the info is there, just that some of it is not as clear as it could be, but I know it’s being improved as we speak.

Leave Comments on Posts

The creator of Staticman was nice enough to create a model Hugo site to demo Staticman working with Hugo. The following were the most helpful examples to set this up with Hugo:

In order for the comment count to be displayed in the article_metadata.html (That’s at the top of each post and listed under each posts title in the list), the blog posts slug needs to be in the front matter of each post:

# Slug is required for counting comments.
slug = "blog-post-file-name-without-md"

If you decide to have Staticman issue a pull request for each reader comment (see step 4 of the getting started guide), you can also set-up a webhook to have Static man delete the branch once you have accepted or closed the pull request.

Subscribe to Comments on Specific Blog Posts

This is where you set-up a Mailgun account and your domain within it. Go ahead and sign up for a free Mailgun account.

The most useful piece of documentation for this was an issue thread in which @eduardoboucas explained how this should work. Some of the following comments on the thread were also useful.

From the staticman.sample.yml which you copied to your sites root directory and modified, based on the directions in the official documentation, you will need to uncomment the #notifications:, #enabled: true and add your encrypted apiKey and domain that Staticman encrypts for you (also discussed in the issue thread mentioned above) if you use the API.

Subscribe to notification by email as new blog posts are published

Once I had the above set-up and working, this step only took an afternoon. What was involved:

  • Added the blogSubscribers section to staticman.yml, I will discus this when we look at the code in the next section
  • Added the post.html (shown below). This will POST your subscription, and display a confirmation screen
  • Some more styling added to override.css
  • On first POST the mailing list will be created in your Mailgun account. I then just gave it a sensible name, so I can see what the purpose of the list is at a glance, as Staticman provides a “MD5 hash of the Github username, repository, and entry id concatenated together” which is prefixed to the Alias Address of the new Mailgun mailing list that you can send notifications to. If you have moderation: true in your staticman.yml under the section responsible for this (blogSubscribers in my case), you will receive a pull request each time someone subscribes.

    If you are like me and would like to address notifications to a name/handle, the following is currently required:

    Once I receive the Github pull request from Staticman for blog subscription, if it looks legitimate, I need to md5sum the email address in the new Mailgun record:

    echo -n [the-email-address-from-mailgun-record] | md5sum

    and compare the result with the hashed email in the pull request. Then take the name from that pull request and apply it to the Mailgun entry. This is a small piece of manual work that would be nice if Staticman could send the name as well and have it added to the email records name variable.

    The pull request can be closed, unless for some reason you want it merged.

Sending Email to Subscribers

Once you have the above set-up, sending the email is as simple as:

curl -s --user 'api:key-[key-hash-goes-here]' \ \
   -F from='<your-name> <email-address-that-subscribers-can-reply-to>' \
   -F to=<your-mailgun-email-address> \
   -F subject='New Blog Post from You' \
   --form-string html='<html>Hi<br><br>This is a link to the new post: <a href="https://<your-domain>/<post-slug>"><name-of-your-post></a><br><br> Enjoy!<br><br>-You.<br><br><br>If at any point you would like to unsubscribe from this mailing list, click <a href="%mailing_list_unsubscribe_url%">Unsubscribe</a></html>'

Show me the Code

Firstly, all of this is up and running on the blog you are reading now.

You may also notice the botpot input field on both forms. This is from a mitigations section in the Web Applications chapter of my second book around captchas, and how they place the website owners problem on the end uers. Bots are not the end users problem, so why should they have to jump through hoops to submit a simple form? Check out my research on the topic.

Enough ranting… The following are the new parts I added to config.toml:

Modified File: config.toml
# Comment out disqusShortname Key/Value pair
#disqusShortname = ""

# Add the following new Table somewhere under the params table
  endpoint = ""
  username = "binarymist"
  repository = "BinaryMistBlog"
  branch = "master"
New File: staticman.yml

This is required by Staticman. You’ll notice I have a comments section and a blogSubscribers section. The former is used by Staticman when you submit a comment on a specific post, you can see this in action below. The latter is used by Staticman when you subscribe to be notified of a new blog post, you can see this in action here. If all you need is blog comments, you only need the comments section.

New Override: /layouts/partials/comments.html

comments.html is used for posting and subscribing to each specific blog post comment thread, and was copied from /themes/academic/layouts/partials/ and modified extensively. The first lines diff:

- {{ if and .Site.DisqusShortname (not (or .Site.Params.disable_comments .Params.disable_comments)) }}
+ {{ if and (or .Site.DisqusShortname .Site.Params.staticman) (not (or .Site.Params.disable_comments .Params.disable_comments)) }}

Lines 3 - 4 and 6 - 71 are brand new lines:

 1{{ if and (or .Site.DisqusShortname .Site.Params.staticman) (not (or .Site.Params.disable_comments .Params.disable_comments)) }}
 2  <section id="comments">
 3    {{ if .Site.DisqusShortname }}
 4      <div class="disqus-comments">
 5        {{ template "_internal/disqus.html" . }}
 6      </div>
 7    {{ end }}
 8    {{ if .Site.Params.staticman }}
 9      <section class="staticman-comments post-comments">
10        <h3>Comments</h3>
12        {{ $comments := readDir "data/comments" }}
13        {{ $.Scratch.Add "hasComments" 0 }}
14        {{ $postSlug := .Source.BaseFileName }}
16        {{ range $comments }}
17          {{ if eq .Name $postSlug }}
18            {{ $.Scratch.Add "hasComments" 1 }}
19            {{ range $index, $comments := (index $.Site.Data.comments $postSlug ) }}            
20              <div id="commentid-{{ ._id }}" class="post-comment">
21                <div class="post-comment-header">
22                  <img class="post-comment-avatar" src="{{ .email }}?s=70&r=pg&d=identicon">
23                  <p class="post-comment-info">
24                    <span class="post-comment-name">{{ .name }}</span>
25                    <br>
26                    <a href="#commentid-{{ ._id }}" title="Permalink to this comment">
27                      <time class="post-time">{{ dateFormat "Monday, Jan 2, 2006 at 15:04 MST" .date }}</time>
28                    </a>
29                  </p>
30                </div>
31                {{ .comment | markdownify }}
32              </div>
33            {{ end }}       
34          {{ end }}
35        {{ end }}
37        {{ if eq ($.Scratch.Get "hasComments") 0 }}
38          <p>Be the first to leave a comment.</p>
39        {{ end }}
41        <h3>Say something</h3>
42        Your email is used for <a href="" target="_blank">Gravatar</a> image and reply notifications only.
45        <form class="post-new-comment" method="post" action="{{ .Site.Params.staticman.endpoint }}/{{ .Site.Params.staticman.username }}/{{ .Site.Params.staticman.repository }}/{{ .Site.Params.staticman.branch }}/comments">
46          <input type="hidden" name="options[redirect]" value="{{ .Permalink }}#comment-submitted">
47          <input type="hidden" name="options[slug]" value="{{ .Source.BaseFileName }}">
48          <input type="hidden" name="fields[postName]" value="{{ .Source.BaseFileName }}"/>          
49          <input type="text" name="fields[name]" class="post-comment-field" placeholder="Name *" required/>
50          <input type="email" name="fields[email]" class="post-comment-field" placeholder="Email address (will not be public) *" required/>          
51          <input type="address" name="fields[botpot]" placeholder="botpot (do not fill!)" style="display: none"></textarea>          
52          <textarea name="fields[comment]" class="post-comment-field" placeholder="Comment (markdown is accepted) *" required rows="10"></textarea>
53          <!-- Following fields used for subscribing to comments -->
54          <input type="hidden" name="options[origin]" value="{{ $.Permalink }}#comments">
55          <input type="hidden" name="options[parent]" value="{{ .Source.BaseFileName }}">
56          <input id="form-submit" type="checkbox" name="options[subscribe]" class="checkbox post-comment-field" value="email">
57          <label for="form-submit" class="post-comment-field checkbox-label">   Notify me of new comments on this post</label>
58          <!-- End following fields used for subscribing to comments -->
59          <input type="submit" class="post-comment-field btn btn-primary comment-buttons" value="Submit">
60        </form>
61      </section>
63      <div id="comment-submitted" class="dialog">
64        <h3>Thank you</h3>
65        <p>Your comment has been submitted and will be published once it has been approved.</p>
66        <p><a href="{{ .Site.Params.staticman.username }}/{{ .Site.Params.staticman.repository }}/pulls">Click here</a> to see the pull request you generated.</p>
68        <p><a href="#" class="btn btn-primary comment-buttons ok">OK</a></p>
69      </div>
71    {{ end }}
72  </section>
73{{ end }}
New Override: /layouts/partials/article_metadata.html

article_metadata.html is used for displaying the comment count directly under the title of each blog post in the post list and at the top of each post. You won’t see the count unless there are actually comments on the post. article_metadata.html was copied from /themes/academic/layouts/partials/ and modified.

Line 21 diff:

-   {{ $comments_enabled := and $.Site.DisqusShortname (not (or $.Site.Params.disable_comments $.Params.disable_comments)) }}
+   {{ $comments_enabled := and (or $.Site.DisqusShortname $.Site.Params.staticman) (not (or $.Site.Params.disable_comments $.Params.disable_comments)) }}

Lines 23 and 27 - 43 are brand new lines:

 1{{ $is_list := .is_list }}
 2{{ $ := .content }}
 3<div class="article-metadata">
 5  <span class="article-date">
 6    {{ if ne $.Params.Lastmod $.Params.Date }}
 7        {{ i18n "last_updated" }}
 8    {{ end }}
 9    <time datetime="{{ $.Date }}" itemprop="datePublished">
10      {{ $.Lastmod.Format $.Site.Params.date_format }}
11    </time>
12  </span>
14  {{ if ne $.Site.Params.reading_time false }}
15  <span class="middot-divider"></span>
16  <span class="article-reading-time">
17    {{ $.ReadingTime }} {{ i18n "minute_read" }}
18  </span>
19  {{ end }}
21  {{ $comments_enabled := and (or $.Site.DisqusShortname $.Site.Params.staticman) (not (or $.Site.Params.disable_comments $.Params.disable_comments)) }}
22  {{ if and $comments_enabled ($.Site.Params.comment_count | default true) }}
23    {{ if $.Site.DisqusShortname }}
24      <span class="middot-divider"></span>
25      <a href="{{ $.Permalink }}#disqus_thread"><!-- Count will be inserted here --></a>
26    {{ end }}
27    {{ if $.Site.Params.staticman }}
28      {{ $.Scratch.Set "commentCountPerPost" 0 }}
29      {{ if $.Slug }} <!-- Can't count comments without slug -->
30        {{ if fileExists (printf "data/comments/%s" $.Slug) }} <!-- If the comment dir exists, we can count comments -->
31          {{ $comments := readDir (printf "data/comments/%s" $.Slug) }}
32          {{ $.Scratch.Set "commentCountPerPost" (len $comments) }}
33        {{ end }}        
34      {{ end }}
35      {{ if gt ( $.Scratch.Get "commentCountPerPost" ) 1 }}
36        <span class="middot-divider"></span>
37        <a href="{{ $.Permalink }}#comments">{{ $.Scratch.Get "commentCountPerPost" }} Comments</a>
38      {{ else if eq ( $.Scratch.Get "commentCountPerPost" ) 1 }}
39        <span class="middot-divider"></span>
40        <a href="{{ $.Permalink }}#comments">1 Comment</a>
41      {{ end }}
42    {{ end }}
43  {{ end}}
45  {{ if isset $.Params "categories" }}
46  {{ $categoriesLen := len $.Params.categories }}
47  {{ if gt $categoriesLen 0 }}
48  <span class="middot-divider"></span>
49  <span class="article-categories">
50    <i class="fa fa-folder"></i>
51    {{ range $k, $v := $.Params.categories }}
52    <a href="{{ "/categories/" | relLangURL }}{{ . | urlize | lower }}">{{ . }}</a
53    >{{ if lt $k (sub $categoriesLen 1) }}, {{ end }}
54    {{ end }}
55  </span>
56  {{ end }}
57  {{ end }}
59  {{ if ne $is_list 1 }}
60  {{ partial "share.html" $ }}
61  {{ end }}
New Override: layouts/section/post.html

post.html is used for subscribing to new blog posts, and was copied from /themes/academic/layouts/section/ and modified. Lines 14 - 40 are brand new lines.

 1{{ partial "header.html" . }}
 2{{ partial "navbar.html" . }}
 4{{ partial "header_image.html" . }}
 6<div class="universal-wrapper">
 8  <h1>{{ .Title | default (i18n "posts") }}</h1>
10  {{ with .Content }}
11  <div class="article-style" itemprop="articleBody">{{ . }}</div>
12  {{ end }}
13  {{ $paginator := .Paginate .Data.Pages }}
14  {{ if eq ( $paginator.PageNumber ) 1 }}
15    {{ .Scratch.Set "redirectUrl" (print .Permalink "#blogsubscription-submitted") }}
16  {{ else }}
17    {{ .Scratch.Set "redirectUrl" (print .Permalink "page/" $paginator.PageNumber "/#blogsubscription-submitted") }}
18  {{ end }}
19  <section class="subscribe-to-blog">
20    <form class="post-blogsubscribe" method="post" action="{{ .Site.Params.staticman.endpoint }}/{{ .Site.Params.staticman.username }}/{{ .Site.Params.staticman.repository }}/{{ .Site.Params.staticman.branch }}/blogSubscribers">
21      <input type="hidden" name="options[redirect]" value="{{ .Scratch.Get "redirectUrl" }}">
22      <input type="hidden" name="options[slug]" value="post-collection">
23      <input type="text" name="fields[name]" class="post-blogsubscriber-field left" placeholder="Name *" required/>
24      <input type="email" name="fields[email]" class="post-blogsubscriber-field right" placeholder="Email address (not publicised) *" required/>          
25      <input type="address" name="fields[botpot]" placeholder="botpot (do not fill!)" style="display: none"></textarea>          
26      <!-- Following fields used for subscription -->
27      <input type="hidden" name="options[origin]" value="{{ $.Permalink }}">
28      <input type="hidden" name="options[parent]" value="post-collection">
29      <input type="hidden" name="options[subscribe]" value="email">
30      <!-- End following fields used for subscription -->
31      <input type="submit" class="btn btn-primary comment-buttons post-blogsubscriber-btn" value="Subscribe to new posts     --     Unsubscribe at any time">
32    </form>
33  </section>
34  <div id="blogsubscription-submitted" class="dialog">
35    <h3>Thank you</h3>
36    <p>Your subscription request has been submitted.</p>
37    <p>You will receive a notification email of new posts when they are published.</p>
38    <p>There will be an unsubscribe link in the notification emails if you wish to unsubscribe.</p>
39    <p><a href="#" class="btn btn-primary comment-buttons ok">OK</a></p>
40  </div>
41  {{ range $paginator.Pages }}
42    {{ $params := dict "post" . }}
43    {{ partial "post_li" $params }}
44  {{ end }}
46  {{ partial "pagination" . }}
49{{ partial "footer_container.html" . }}
50{{ partial "footer.html" . }}
Modified File: override.css

In the config.toml, you can provide style overrides:

custom_css = ["override.css"]

The relevant styling is all commented and looks like the following:

122/* Staticman comment section and form */
123 {
125   margin-top: 60px;
127 {
129   background-color: rgb(247, 247, 247);
130   padding: 20px;
131   margin-top: 20px;
133 {
135   margin-bottom: 20px;
137 {
139   display: inline-block;
140   vertical-align: middle;
141   border-radius: 50%;
143 {
145   display: inline-block;
146   margin-left: 20px;
147   margin-bottom: 0;
148   vertical-align: middle;
151/* Part of blog subscription also */, .post-blogsubscriber-btn {
153   display: block;
154   font: inherit;
155   padding: 10px;
156   margin-top: 20px;
157   outline-color: #9b6bcc;
158   width: 100%;
161.btn-primary.comment-buttons {
162   background: #9b6bcc !important;
163   border-color: #9b6bcc !important;
164   font-size: 0.9rem;
165   padding: 10px 14px 9px;
166   border-radius: 6px;
169.btn-primary.comment-buttons:hover {
170   background: #53237f !important;
172 .post-comment-name {
174   font-size: 1.4rem;
175   font-weight: 500;
178 .post-time {
180   font-size: 14px;
181   font-weight: normal;
182   letter-spacing: 0.03em;
183   color: #888;
186 .post-time:hover {
188   color: #9b6bcc;
192/* End staticman comment section and form */
194/* Staticman comment submission confirmation dialog */
196.dialog {
197   display: none;
198   position: fixed;
199   background-color: rgb(247, 247, 247);
200   padding: 25px;
201   padding-top: 20%;
202   top: 0;
203   left: 0;
204   width: 100%;
205   height: 100%;
206   text-align: center;
209.dialog:target {
210   display: block;
213.dialog .btn-primary.comment-buttons.ok {
214   width: 7rem;
217/* End staticman comment submission confirmation dialog */
219/* Notify me of new comments checkbox */
221input[type=checkbox].checkbox {
222   display: none;
225.checkbox-label {
226   position: relative;
227   padding-left: 0px;
228   padding-bottom: 0px;
229   margin-top: 10px;
230   margin-bottom: 15px;
231   float: left;
234.checkbox-label:before {
235   content: ' ';
236   display: inline-block;
237   width: 25px;
238   height: 25px;
239   border-width: 1px;
240   border-style: solid;
241   vertical-align: middle;
242   position: relative;
243   bottom: 2px;color: rgb(169, 169, 169);    
246.checkbox:checked+.checkbox-label:after {
247   content: 'x';
248   display: inline-block;
249   position: absolute;
250   width: 25px;
251   height: 25px;
252   border-width: 2px;
253   line-height: 25px;
254   top: 11px;
255   left: 1px;
256   font-family: sans-serif;
257   text-align: center;
260/* End notify me of new comments checkbox */
262/* Subscribe to blog posts */
263 {
265   clear: left;
266   float: left;
267   font: inherit;
268   padding: 10px;
269   margin-top: 20px;
270   margin-bottom: 20px;
271   outline-color: #9b6bcc;
272   width: 48%;  
274 {
276   clear: none;
277   float: right;
278   font: inherit;
279   padding: 10px;
280   margin-top: 20px;
281   margin-bottom: 20px;
282   outline-color: #9b6bcc;
283   width: 48%;  
286/* End subscribe to blog posts */

Contributing back to the Hugo Academic theme

The changes we’ve just been discussing have now been submitted back to mainline Hugo Academic theme.




Great tutorial!

Staticman is the only commenting system that I would use for my blog. Unfortunately it’s not yet officially supported by Hugo. Your post saved me a lot of time!

Thanks a lot!


Did you get it working on your site?


This is fantastic. I’m following your PR on Github. Hopefully we’ll see some official implementation soon! How does unsubscribing work? Simply through the notification email that Mailgun sends?

Looking around several months ago, this site: here shows Jekyll with Staticman commenting. They implemented Nested Commenting which I thought was interesting.

Thanks for all your hard work. Cheers :)


Yeah, mailgun sends an unsubscribe link in each notification.

I looked at nested commenting also, it was quite a bit more work, and I actually prefer the non nested look/flow.

You can also subscribe to the entire blog here.

Cheers :-)


@binarymist: After posting my comment, I received a notification email (for your reply), but when I came here to check, there was nothing other than my comment. Then I received two other emails, and again there was nothing. I thought it was a bug, so I unsubscribed. Only today when I came back, the comments are displayed. There must be some caching issue (with CloudFlare?).


I’m pretty sure it’s your browser, as I purge the Cloudflare cache after the static files are built, then refresh my browser and the new comments are visible.


Just want to chime in. I’m also subscribed to comments on this post. I’m getting notifications left and right, but when I click on the link, the page loads and nothing new appears.

Is Mailgun set up to notify even though you haven’t accepted the comment in Github yet? Or is it only when the comment is approved and shown on the page?


I think staticman informs mailgun of the merge, then you get the notification. Then I need to: 1. pull 2. perform any merges 3. generate the new site locally 4. run the script 5. Purge the cloudflare cache, which can take about 30 to 45 seconds 6. Refresh the browser cache

If you go looking for the new comment(s) before step 1, 2, 3, 4, 5 have been completed, and then you perform step 6, then you won’t see new comments.


Ah yes I understand, but just for reference, I received 8 email notifications for the 4 new comments above (March 12).


OK, that’s great to know, My main concern currently is how many notifications anyone gets when a comment is submitted. There are currently three people subscribed to this post, I seem to be getting three notifications when anyone of us submits a comment and I accept it. Sounds like you are getting two notifications for every accepted PR. This seems like a bug. I’ll discuss with Staticman.

Thanks a lot for this.


Issue ( submitted. Please review and comment as necessary.


In my last comment I already checked “Notify me of new comments on this post” (because I had unsubscribed before), but I did not receive any emails (that’s why here I am a bit late).

Say something

Your email is used for Gravatar image and reply notifications only.

Thank you

Your comment has been submitted and will be published once it has been approved.

Click here to see the pull request you generated.