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/.


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 &lt;your.email<span>@</span>example.org&gt;
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" -}}
	<h3 class="comment-header">
	  {{ if $comment.link }}
	  <a href="{{ $comment.link }}">{{ $comment.author }}</a>
	  {{ else }}
	  {{ $comment.author }}
	  {{ end }}
	  <br />
	{{ $comment.comment | markdownify }}
  {{ end }}


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!

# Copyright (C) 2016-2021 Ryan Kavanagh <rak@rak.ac>
# Distributed under the ISC license



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')


# 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`.


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`.


You can see the rendered output at the bottom of this page.