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:
- Leave comments on posts
- Subscribe to comments on specific blog posts
- 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:
01486cfc6aa638a6f8e85142c645fcd7
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.
- The Beautifulhugo theme appear(es|ed) to support Staticman, but there was no relevant config in the
config.toml
orstaticman.yml
. Let me know if I have missed some of their documentation that explains the required config? Thesingle.html
layout andstaticman-comments.html
was marginally helpful - The gohugohq howto was also marginally helpful.
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:
- Demo site: https://hugo.staticman.net/post/my-entry/
- Config file: https://github.com/eduardoboucas/hugo-plus-staticman/blob/master/staticman.yml
- The layout partial that handles the markup for the comment display and posting: https://github.com/eduardoboucas/hugo-plus-staticman/blob/master/themes/hugo-type-theme/layouts/partials/post-comments.html
- I used the style-sheet from the Staticman creators own website (source) for the “Notify me of new comments on this post” checkbox as a starting point
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 tostaticman.yml
, I will discus this when we look at the code in the next section -
Added the
post.html
(shown below). This willPOST
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 havemoderation: true
in yourstaticman.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 recordsname
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]' \
https://api.mailgun.net/v3/mailgun.binarymist.io/messages \
-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 %recipient.name%.<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
[params.staticman]
endpoint = "https://api.staticman.net/v2/entry"
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>
11
12 {{ $comments := readDir "data/comments" }}
13 {{ $.Scratch.Add "hasComments" 0 }}
14 {{ $postSlug := .Source.BaseFileName }}
15
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="https://www.gravatar.com/avatar/{{ .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 }}
36
37 {{ if eq ($.Scratch.Get "hasComments") 0 }}
38 <p>Be the first to leave a comment.</p>
39 {{ end }}
40
41 <h3>Say something</h3>
42 Your email is used for <a href="https://gravatar.com" target="_blank">Gravatar</a> image and reply notifications only.
43
44
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>
62
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="https://github.com/{{ .Site.Params.staticman.username }}/{{ .Site.Params.staticman.repository }}/pulls">Click here</a> to see the pull request you generated.</p>
67
68 <p><a href="#" class="btn btn-primary comment-buttons ok">OK</a></p>
69 </div>
70
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">
4
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>
13
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 }}
20
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}}
44
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 }}
58
59 {{ if ne $is_list 1 }}
60 {{ partial "share.html" $ }}
61 {{ end }}
62
63</div>
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" . }}
3
4{{ partial "header_image.html" . }}
5
6<div class="universal-wrapper">
7
8 <h1>{{ .Title | default (i18n "posts") }}</h1>
9
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 }}
45
46 {{ partial "pagination" . }}
47
48</div>
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
124.post-comments {
125 margin-top: 60px;
126}
127
128.post-comment {
129 background-color: rgb(247, 247, 247);
130 padding: 20px;
131 margin-top: 20px;
132}
133
134.post-comment-header {
135 margin-bottom: 20px;
136}
137
138.post-comment-avatar {
139 display: inline-block;
140 vertical-align: middle;
141 border-radius: 50%;
142}
143
144.post-comment-info {
145 display: inline-block;
146 margin-left: 20px;
147 margin-bottom: 0;
148 vertical-align: middle;
149}
150
151/* Part of blog subscription also */
152.post-comment-field, .post-blogsubscriber-btn {
153 display: block;
154 font: inherit;
155 padding: 10px;
156 margin-top: 20px;
157 outline-color: #9b6bcc;
158 width: 100%;
159}
160
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;
167}
168
169.btn-primary.comment-buttons:hover {
170 background: #53237f !important;
171}
172
173.post-comment-info .post-comment-name {
174 font-size: 1.4rem;
175 font-weight: 500;
176
177}
178
179.post-comment-info .post-time {
180 font-size: 14px;
181 font-weight: normal;
182 letter-spacing: 0.03em;
183 color: #888;
184
185}
186
187.post-comment-info .post-time:hover {
188 color: #9b6bcc;
189}
190
191
192/* End staticman comment section and form */
193
194/* Staticman comment submission confirmation dialog */
195
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;
207}
208
209.dialog:target {
210 display: block;
211}
212
213.dialog .btn-primary.comment-buttons.ok {
214 width: 7rem;
215}
216
217/* End staticman comment submission confirmation dialog */
218
219/* Notify me of new comments checkbox */
220
221input[type=checkbox].checkbox {
222 display: none;
223}
224
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;
232}
233
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);
244}
245
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;
258}
259
260/* End notify me of new comments checkbox */
261
262/* Subscribe to blog posts */
263
264.post-blogsubscriber-field.left {
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%;
273}
274
275.post-blogsubscriber-field.right {
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%;
284}
285
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.
Comments
f10w
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!
binarymist
cyp
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 :)
binarymist
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 :-)
f10w
binarymist
cyp
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?
binarymist
I think staticman informs mailgun of the merge, then you get the notification. Then I need to:
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.
cyp
binarymist
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.
binarymsit
f10w
Jim
Nghi Nguyen
binarymist
Submission of new comments has been disabled for this post. Contact us if you have additional comments.