Cactus Comments in WriteFreely

I have been trying out a matrix-based blog comments system.

WriteFreely is a simple self-hosted blogging system. It uses Markdown for content. To let readers subscribe to follow new posts, it supports both RSS and ActivityPub (Fediverse). It has no comments system of its own.

Cactus Comments is a simple self-hosted comments system. It lets us add a comments section to any web page we control, such as a blog. It uses Matrix for the comments.

I describe a self-hosted deployment.

Why Matrix-Based Comments?

Here's a tag line and a longer explanation I just dreamed up:

“Cactus Comments: your comments are your data. Own them!”

Unlike other comment systems, Cactus Comments is not a warehouse, but a post office. Your comments don't just go into a database owned by somebody else. What you write in Cactus Comments is sent from your own regular matrix account. This means you interact on an equal footing: for example, if the site operator may choose not to display your comment, but they don't have control of the only copy of it, you get to keep your own copy of whatever you write, as long as you like. And if you replied to someone, or someone replied to your comment, you are free to keep a copy of their comments too, even after those people close their accounts. That's an example of what we mean when we speak of the freedom brought by using an open system built on open protocols: the freedom not to be controlled absolutely by some other person or company's system, but for you to choose how the system works for you and maintain some control over your own participation.

Installing Cactus Comments and Configuring Matrix

I deploy my matrix server(s) using matrix-docker-ansible-deploy. Its documentation describes setting up Cactus Comments. Basically there is little more to it than adding the following two variables to our Ansible inventory.

matrix_cactus_comments_enabled: true
matrix_synapse_allow_guest_access: true

See “About Guest Access” in the “Why Like This?” section below.

Registering a Site Name

As documented, start a matrix chat with the bot and tell it, register SITE_NAME where the SITE_NAME you choose must match the same in the client configuration script below.

JulianF created this DM JulianF invited Cactus Comments

JulianF register SITE_NAME

Cactus Comments Created site SITE_NAME for you

If the bot does not respond, double-check the installation. When I first set it up, I tried to take a short cut by running Ansible with only the tags install-cactus-comments,start but this omitted the needed configuration of synapse and the bot gave no response. After running ansible-playbook [...] --tags=setup-all,start, as stated in the documentation, it worked.

Configuring WriteFreely

On the WF blog's “Customize” page, insert the following code in the box under the heading “Custom CSS”. If you want any real custom CSS, you can put it before and/or after this.

Why are we using the “Custom CSS” for a script? It is explained in the “Why Like This?” section below.

</style>
<script type="text/javascript" src="https://matrix.HOMESERVER_DOMAIN/cactus-comments/cactus.js"></script>
<link rel="stylesheet" href="https://matrix.HOMESERVER_DOMAIN/cactus-comments/style.css" type="text/css">
<script>
document.addEventListener('DOMContentLoaded', () => {
  const commentSectionId = new URL(document.head.querySelector('link[rel="canonical"]').href).pathname
  initComments({
    node: "#comment-section",
    defaultHomeserverUrl: "https://matrix.HOMESERVER_DOMAIN",
    serverName: "HOMESERVER_DOMAIN",
    siteName: "SITE_NAME",
    commentSectionId: commentSectionId,
    guestPostingEnabled: false
  })
})
</script>
<style>

Check and adjust the following:

On the same WriteFreely “Customize” page, insert the following code in the box under the heading “Post Signature”. If you want any real post signature, it can go before and/or after this comment-section.

<div id="comment-section"></div>

There is nothing we need to check or adjust in this part.

Why Like This?

In this section we look at some of the technical particulars and discuss why did them this way.

About Guest Access

If we read the docs for the matrix_synapse_allow_guest_access setting, they say it is “to allow guest comments”, but unlike the optional client-side setting for guest posting, that we will get to later, Cactus Comments requires guest access to be enabled on its matrix server. What for? It does indeed use a guest account for the (optional) guest mode of posting, but, in addition to that, it also uses a guest account in order to display the existing comments to a user who is not logged in. If we do not enable guest access here, what happens? Cactus Comments displays an error message in red above the comment box, and does not display any existing comments, unless and until the reader logs in to their own matrix account.

I am presently wondering whether allowing guest access will open up my matrix server too much for abuse. I am not sure. The “guest access” feature is little used in the matrix world, and information about it, and managing it, is scarce.

In the meantime, I am deploying this instance of Cactus Comments on a secondary matrix server, rather than my main one. That doesn't stop me using my main matrix account (on my main matrix server) to write my own comments, of course. Matrix is federated, that's the whole point of it, so I and any other commenters can each log in on our chosen matrix homeserver, and still read and write these comments.

Why Use the “Custom CSS” Configuration?

WriteFreely provides some customisation options but not complete control. Of course it is open source so one can do anything at the source code level. Still, for a quick start, it's nice to find a way that uses an unmodified installation.

Another user gave me the hint that WF's “Custom CSS” setting can be mis-used to inject a <script> element into the <head> section of the blog page's HTML. It is a typical code-injection hack. Knowing that the content of this box is going to be placed verbatim between <style> and </style> tags in the document head, we start by closing the <style> element, then we place our <script> ... </script>, and then we open a new <style> element.

The addEventListener

We are injecting our script in the document <head>, which comes before our “comment-section” div which we put near the end of the HTML document. The parameter node: "#comment-section" tells initComments to look up the “comment-section” element. Because in-line script is executed as soon as it is encountered, a simple attempt here to look up the later “comment-section” element would fail: that later part of the DOM content has not yet been loaded at this point. Instead we need to wait until the DOM content is loaded before we will be able to look up the “comment-section” element. That is why we use addEventListener.

Two ways to supply the node parameter are documented. The alternative to passing in a querySelector like "#comment-section" is to look up the required element before we call initComments, and pass in a reference to the node object like this: node: document.getElementById("comment-section"). But that does not change the requirement that the DOM content needs to be loaded first.

If we placed our script at the end of the HTML document, after the “comment-section” div, that would change things. Then this delayed execution would not be necessary.

(I learnt the general form of this solution from Mitja Felicijan who noted it in the Cactus Comments discussion room last October. I changed it to use DOMContentLoaded instead of window.addEventListener('load'), after I read in MDN says “It is a common mistake to use load where DOMContentLoaded would be more appropriate.“)

The commentSectionId

The commentSectionId needs to be unique per blog post, for the way most of us want our blogs to behave. The documentation (both in Cactus Comments and in matrix-docker-ansible-deploy) shows it configured as a static value such as “1”. Doing that would share the same set of comments across all posts, not what we want.

Different blog systems have different ways of identifying a post. WriteFreely includes a canonical URL in the document head. For a published post, it looks like:

<link rel="canonical" href="https://wrily.foad.me.uk/the-post-slug" />

We get the slug (more precisely, the pathname part including leading slash) using javascript: new URL(document.head.querySelector('link[rel="canonical"]').href).pathname.

Cactus Comments uses the commentSectionId to form a matrix room name, something like, #comments_wrily_%2Fthe-post-slug:HOMESERVER_DOMAIN. It stores the comments for this blog post in this room. The room name prefix #comments_ is configured on the server side, and the next part wrily is the SITE_NAME we configured on the client side.

Moderation, Spam control, Administration

Cactus Comments provides limited moderation facilities on its own. It creates a moderation room where it says,

🚨 If you ban someone from one of your comment sections, I'll make sure they're banned from all of your comment sections 👊 If you add a moderator to this room, I'll make sure they have the same permissions across all your comment sections 👮‍♀️👮‍♂️

For more sophisticated spam control, deployment of Mjolnir was suggested.

What Does the End Result Look Like?

If my deployment is working properly, you should be able to see the result here in this blog page. Please respect this, leave only relevant comments, and don't use it for testing.

The Cactus.Chat home page invites you to Try the Demo which you can use for testing.


Feedback:

Donations gratefully accepted