On Wednesday April 3, 2019, I finished migrating this site from Wordpress to Hugo. An important part of this project was finding a new system for handling blog post comments.

On my personal blog, I am able to use the free and ad-free tier of Disqus, but because vxlabs.com is strictly-speaking a company site (even although 100% of the posts are non-commercial), that won’t work here.

Furthermore, the $9 / month price tag of the lowest Disqus tier is not really justifiable with the relatively small number of comments being handled.

I had to find an alternative, preferably self-hosted solution.

I ended up selecting Isso, for the following reasons:

  • Implemented in Python, which I use daily, so I am easily able to understand and modify source if required.
  • Like Wordpress, supports moderated commenting without logging in.
  • Large enough community and good support in the Hugo world.

(Other candidates high up on my list include commento, which has hosted and self-hosted open source versions, and mouthful.)

In this post, I document how I installed this on WebFaction, my webhoster. My installation makes use of uwsgi, and mounts isso on a sub-path of the same host as the blog.

Create custom web-app.

In webfaction, I created a custom web-application listening on a dedicated port, only open to localhost.

The general idea is to setup a cron-job which will keep uwsgi (with isso) running behind that dedicated port. That web-app can then be mounted on a sub-path, say /isso/ of the server.

Any requests to vxlabs.com/isso/ will be forwarded to the running uwsgi-isso processes.

Setup Python environment using pipenv.

pipenv is a great tool for managing the Python virtual environment required for isso.

Install pipenv in your Python user packages by invoking the following command:

1
pip3 install --user pipenv

After pipenv has installed, initialise the Python virtual environment in the webapp’s directory as follows:

1
2
cd /to/your/webapp/
pipenv install isso uwsgi

This will install isso, uwsgi and all of their dependencies into a newly created virtual environment.

Pretty straight-forward, no?

Configure isso.

In the web-app’s directory, create the following isso.cfg configuration file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
[general]
; create sqlite3 database in the webapp directory
dbpath = ~/webapps/isso_vxlabs/comments.db
; this is the URL of the blog hosting the comments
host = https://vxlabs.com/
; send notifications to email and to uwsgi log file
notify = smtp,stdout
; allow users to request mail notifications on new comments
reply-notifications = true
gravatar = true

[server]
; replace 1337 with the dedicated webapp port
; this is only required for standalone execution of isso, not for uwsgi
listen = http://localhost:l337/
; NB: required when you host a single isso on a sub-domain
public-endpoint = https://vxlabs.com/isso

[admin]
enabled = true
password = your_secret_password

[moderation]
enabled = true

[guard]
; I want users to enter their email addresses
require-email = true

[smtp]
; I configured a dedicated SMTP account for this
; see the isso documentation

Importantly here, I receive an email to moderate every single comment. Clicking on the link in the email, and then confirming via a browser dialog is all that is required to approve the comment.

Import comments from existing blog.

I migrated all comments from the old wordpress blog following the procedure documented in the isso manual.

This scanned the wordpress blog’s full export XML and proceeded to import about a thousand comments in the blink of an eye.

Setup uwsgi.

Altough you can run isso directly during development, that’s not recommended for production.

Because I’ve had great experience with uwsgi in the past for running Django and Flask apps in production, I opted to use uwsgi as the application server also in this case.

Start by creating the following file as isso_vxlabs_uwsgi.ini in the web-app directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
[uwsgi]
# replace with your dedicated port
http = :1337
master = true
processes = 4
cache2 = name=hash,items=10240,blocksize=32
# create uwsgi_mail in the directory containing this ini
spooler = %d/uwsgi_mail
module = isso.run
virtualenv = $(VIRTUAL_ENV)
# isso.cfg is in the same dir as this ini
env = ISSO_SETTINGS=%d/isso.cfg
# detach and log to file
daemonize = %d/uwsgi.log
# store pid so we can easily kill -INT
pidfile = %d/uwsgi.pid

On WebFaction, there is no direct access to something like systemd or upstart for starting and keeping our uwsgi running, so I made the following bash script which can be added to the user’s local crontab:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

# check if isso uwsgi is running - if not, start it!
# have crontab run this every 5 minutes by adding the following entry:
# */5 * * * * ~/your_webapp_dir/maybe-start-isso-vxlabs-uwsgi.sh

MYDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
RUNNING=$(ps ax | grep isso_vxlabs_uwsgi | grep -v grep)

if [ -z "$RUNNING" ];
then cd $MYDIR && $HOME/bin/pipenv run uwsgi isso_vxlabs_uwsgi.ini
fi

Create this as maybe_run_isso.sh in the web-app’s directory and remember to make it executable with chmod +x maybe_run_isso.sh.

Add it to the local crontab with crontab -e. See the script comment above for a suitable crontab entry.

For the initial startup, you can just invoke the maybe script. It will start up and detach from the terminal. Check uwsgi.log for any relevant messages.

Show Isso comments on your posts.

To show comments, I added the following bit of code to the relevant Hugo template:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
{{/* see https://posativ.org/isso/docs/configuration/client/ */}}
{{ if and .IsPage (ne .Params.comment false) -}}
    <script data-isso="//vxlabs.com/isso"
            {{/* users can subscribe to be emailed when new comments are made */}}
            data-isso-reply-notifications="true"
            data-isso-avatar="false"
            data-isso-gravatar="true"
            {{/* without this, deeply nested comments are simply not shown! */}}
            data-isso-max-comments-nested="inf"
            data-isso-max-comments-top="inf"
            data-isso-require-email="true"
            src="/isso/js/embed.min.js"></script>

    {{/* cpbotha: add space for isso thread */}}
    {{/* cpbotha: SUPER NB - without title isso fails finding title and can't send email */}}
    <section data-title="{{ .Title }}" id="isso-thread"></section>
{{- end }}

Bonus round: Daily space-efficient backup of your isso comments using hard links.

I made the following script to run a daily backup of the comments database.

It uses rsync’s --link-dest functionality, meaning it will simply hard link the database backup if it hasn’t changed. The upshot of this is that this will only take up space if the database has actually changed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
#!/bin/bash
# invoke with a daily cronjob, e.g.:
# 15 0 * * * ~/your_webapp_dir/backup_comments.sh

MYDIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"

cd $MYDIR
# find three most recently modified 
LDS=$(printf " --link-dest=$MYDIR/backups/%q/ " `ls -1t backups/ | tail -3`)
BDIR=backups/`date +%G%m%d-%H%Mu`
mkdir -p $BDIR
# -ap args are important to preserve all attributes so that hardlinks work
rsync -ap $LDS comments.db $BDIR

Features I would like to have.

While I am happy with isso as it stands, there are a few features I would like to have, and might consider contributing in the future:

  • Only moderate if new email: Ideally, moderation will only be required for new email addresses, with expiry built in. In other words, if a user has made an approved comment within the past month (expiry configurable), new comments are automatically approved with notification to admin.
  • Visual transitions during commenting: When a user comments, scroll to the position of their new comment, and transition / fade it in, with visual focus queues on the “in moderation queue” text.

Wrap-up.

isso is a simple but effective solution for blog comments.

Let me know in the comments below if you have any questions or additions!