Static Comments in Hugo
I switched from Jekyll to Hugo last week for a variety of reasons. One thing that was missing was a port of the “jekyll-static-comments” plugin that I used to use. I liked it because it saved readers from being tracked by Disqus or other comments solutions, and it required no javascript.
To comment, users would email me their comment following a template attached to the bottom of each post. I then piped their email through a script to add it to the right post. As an added benefit, I could delegate comment spam detection to my mail server.
I’ve managed to reimplement this setup using Hugo. For those who are interested in a similar setup, here is what you need to do.
Pages with comments
Instead of being single files, pages need to be leaf bundles.
For example, this means that your blog post must be located at /content/blog/2021-03-12-static-comments-in-hugo/index.md
instead of /content/blog/2021-03-12-static-comments-in-hugo.md
.
This lets you store the comments as page resources in the subdirectory /content/blog/2021-03-12-static-comments-in-hugo/comments/
.
Partials
You should create a comments.html
partial and include it in the layout for the pages which should get comments:
<div class="post-comments">
<p class="comment-notice"><b>Comments</b>: To comment on this post,
send me an email following the template below. Your email address
will not be posted, unless you choose to include it in
the <span style="font-family: monospace;">link:</span> field.</p>
<pre class="comment-notice">
To: Your Name <your.email<span>@</span>example.org>
Subject: [blog-comment] {{ .Page.RelPermalink }}
post_id: {{ .Page.RelPermalink }}
author: [How should you be identified? Usually your name or "Anonymous"]
link: [optional link to your website]
Your comments here. Markdown syntax accepted.</pre>
{{ $scratch := newScratch }}
{{ $scratch.Set "comments" (.Resources.Match "comments/*yml") }}
{{ if eq 1 (len ($scratch.Get "comments")) }}
<h2>1 Comment</h2>
{{ else }}
<h2>{{ len ($scratch.Get "comments") }} Comments</h2>
{{ end }}
{{ range ($scratch.Get "comments") }}
<div class="post-comment {% cycle 'odd', 'even' %}">
{{ $comment := (.Content | transform.Unmarshal) }}
<span class="post-meta">
{{- $comment.date | dateFormat "Jan 2, 2006 at 15:04" -}}
</span>
<h3 class="comment-header">
{{ if $comment.link }}
<a href="{{ $comment.link }}">{{ $comment.author }}</a>
{{ else }}
{{ $comment.author }}
{{ end }}
<br />
</h3>
{{ $comment.comment | markdownify }}
</div>
{{ end }}
</div>
Comments
To associate comments received by email to posts, I pipe them from mutt (using the |
keybinding) to the following (admittedly janky) shell script.
It takes the comment, reformats it appropriately, and puts it in the post’s comments
subdirectory.
Note that it determines which filename to use based on the email’s contents, so make sure to check that the email doesn’t contain anything nefarious before you pipe it into the script!
#!/bin/sh
# Copyright (C) 2016-2021 Ryan Kavanagh <rak@rak.ac>
# Distributed under the ISC license
BLOG_BASE="/media/t/work/blog"
MESSAGE=$(cat)
EMAIL=$(echo "${MESSAGE}" | grep "From:" | sed -e 's/From[^<]*<\?\([^>]*\)>\?.*/\1/g;s/@/-at-/g')
DATE=$(echo "${MESSAGE}" | grep "Date:" | sed -e 's/Date:\s*//g' | xargs -0 date -Iseconds -u -d)
POST_ID=$(echo "${MESSAGE}" | grep "post_id:" | sed -e 's/post_id: //g')
COMMENTS_DIR="${BLOG_BASE}/content/${POST_ID}/comments/"
COMMENT_FILE="${COMMENTS_DIR}/${DATE}_${EMAIL}.yml"
# Strip out the email headers and whitespace until the start of the comment
COMMENT_WHOLE=$(echo "${MESSAGE}" | sed -e '/^\s*$/,$!d;/^[^\s]/,$!d')
# Indent everything after the comment header
COMMENT_INDENTED=$(echo "${COMMENT_WHOLE}" | sed -e '/^\s*$/,${s/.*/ &/g}')
# And add the comment header
COMMENT_PREFIXED=$(echo "${COMMENT_INDENTED}" | sed -e '0,/^\s*$/{s/^\s*$/comment: |/}')
[ -d "${COMMENTS_DIR}" ] || mkdir -p "${COMMENTS_DIR}"
echo "Saving the comment to ${COMMENT_FILE}"
echo "date: ${DATE}" | tee "${COMMENT_FILE}"
echo "${COMMENT_PREFIXED}" | tee -a "${COMMENT_FILE}"
For example, the following comment in an email body:
post_id: /blog/2021-03-12-static-comments-in-hugo/
author: Ryan Kavanagh
link: https://rak.ac/
Dear self,
Here is a test comment for your blog post.
It supports *markdown* **syntax** and `stuff`.
Best,
Yourself
results in a file content/blog/2021-03-12-static-comments-in-hugo/comments/2021-03-12T18:47:25+00:00_rak-at-example.org.yml
containing:
date: 2021-03-12T18:47:25+00:00
post_id: /blog/2021-03-12-static-comments-in-hugo/
author: Ryan Kavanagh
link: https://rak.ac/
comment: |
Dear self,
Here is a test comment for your blog post.
It supports *markdown* **syntax** and `stuff`.
Best,
Yourself
You can see the rendered output at the bottom of this page.
Comments: To comment on this post, send me an email following the template below. Your email address will not be posted, unless you choose to include it in the link: field. If your web browser is configured to handle mailto: links, click comment to load the template into your mail client.
To: Ryan Kavanagh <rak@rak.ac> Subject: [blog-comment] /blog/2021-03-12-static-comments-in-hugo/ post_id: /blog/2021-03-12-static-comments-in-hugo/ author: [How should you be identified? Usually your name or "Anonymous"] link: [optional link to your website] Your comments here. Markdown syntax accepted.
6 Comments
Ryan Kavanagh
Dear self,
Here is a test comment for your blog post. It supports markdown syntax and
stuff
.Best, Yourself
pabs
You might want to encode your comment template into a mailto: link, so that people can just click on the link to reply to your blog, instead of having to copy and paste things to their MUA.
The subject and body query parameters can be used to set the Subject field and body of the email, and the main URL goes into the To field.
All of the parameters should be URL encoded.
For example:
bye, pabs
Ryan Kavanagh
I never thought of that, thanks for the suggestion! I’ve implemented it now :-) For those who are curious about my implementation, here is what I added to my comments template:
Best, Ryan
Steve Kemp
I hope you filter out bogus post-ids, such as
../../../../../.ssh/authorized_keys
before you run your script.I suspect otherwise the user could write to many strange places. Even run commands, if the link-id was $(uptime | mail steve@steve.fi).
Ryan Kavanagh
On Sun, Mar 14, 2021 at 04:26:02PM +0200, Steve Kemp wrote:
Indeed, “Note that it determines which filename to use based on the email’s contents, so make sure to check that the email doesn’t contain anything nefarious before you pipe it into the script!”
I get a grand total of ~6 comments per year, so I just manually review and edit the message in mutt to delete obnoxious stuff (like the above link ;-) ) before piping it through.
The script is broken in many other ways too. For example,
Date:
only appears once in a messageSome day (TM) I will write a better, safer script.
Best, Ryan
JP Caruana
Hi, great post, thanks for sharing! I implemented your solution for displaying comments (not for receiving them), and I found out you can simplify your template code as you don’t need to define a
newScratch
. The following code works fine for me: