Naive comments system
This website used Cactus comments system, which leverages Matrix protocol. It was a nice little experiment but it’s a half baked project, which didn’t grow in the last few years. I have decided to make my own comment system. Something as simple as possible, where comments are stored in the most universal way possible. Because systems change, but data? Data is forever.
The simplest comment system is an interface to send text data, a way to store it in a structured manner and an interface to retrieve that data or its subset, to present it.
What I personally need on top of those foundations is:
- the ability to notify the host (that’s me) whenever a new comment gets created
- the ability to easily filter out things I wouldn’t like to host on my website (slurs, etc.)
I have built my own comments system, using technologies I know decently well:
- Flask - Python is just good for quick prototypes. I hope to move to Nim with Jester. I enjoy using Nim, but I’m significantly slower writing it.
- HTMX - I’m an embedded developer and that’s the only way for me to enjoy making frontends.
As an embedded developer, it’s difficult to admit, but I like making dynamic websites. I like figuring out the HTML + CSS but I also like writing the server and the frontend code. That statement might not hold up if you take away HTMX from me. I’m not interested in using npm or any JavaScript framework. The problems I’m solving are usually solved well without anything more complex than a single dependency of HTMX.
If you’re interested in getting into web development, without the madness it currently seems to be, I recommend reading HTMX’s author article, on what REST originally was about. HYPERMEDIA SYSTEMS provides a full book on how to develop responsive web pages, using HTMX.
You can find my comment’s system on Github: https://github.com/MrOneTwo/silly-comments
Here’s a quick walk-through on how this comment system works.
When you access a webpage containing the following code snippet, your browser initiates an
additional request, to another server. That’s possible thanks to the HTMX library.
I’m using Hugo to build this website so the snippet below is Go template syntax.
All the attributes starting with hx
are introduced by HTMX.
The important part is the hx-get="https://mydomain.com:24000/naive"
- this attribute
instructs HTMX to send the HTTP GET request, to the specified address.
The next attribute is the hx-vals
. This attribute appends the URL parameters and in this
specific example I use the webpage slug, e.g.: for https://ciesie.com/project/magknob
the resulting request URL would be https://mydomain.com:24000/naive?for=/project/magknob
.
I’ll explain soon why I’m using a path-like value for the for
key.
I’d like to mention two remaining attributes: hx-swap
and hx-trigger
.
The former instructs HTMX to replace this entire div
with the server’s response (as opposed
to, for example, inserting the response inside this div
) and the latter tells it to initiate
the request, after the webpage loads.
{{ $path := "https://mydomain.com:24000/naive" }}
<div style="grid-row: 1"
id="comments"
hx-get={{ $path }}
hx-vals='{"for": "{{ .Page.RelPermalink }}" }'
hx-swap="outerHTML"
hx-trigger="load">
</div>
In simpler terms, you open a webpage that knows how to fetch its own comments and does it, through HTMX.
Let me explain the for=/project/magknob
. I have decided to store the comments as files. Each
comment is a separate file. The first line stores the author information and the rest of the file
is the actual comment. The /project/magknob
maps to a path in the comments filesystem. Whenever
you send the earlier discussed GET request, the server reads all the files in the
/project/magknob
directory, builds the HTML representation of the contents of those files,
appends HTML for the new comment submission form and sends it back to the client - your browser.
The HTMX receives that response and swaps the entire div
with the reponse’s content.
Whenever you submit a new comment, the server creates a new file, using ULID for the file’s name. That gives the file a unique name, with the creation timestamp information.
Simple. Naive even. I do not allow access to any path specified as value of the for
key.
That’d expose my entire filesystem. The server stores a list of allowed paths.
Currently that’s a bit of a pain point. The server has to have an up to date list of
supported paths. At the moment of writing this article, some of my pages won’t show the comments
or the comment submission form, because I didn’t whitelist those URLs on the server side. There’s
no clever automation here. Just a file that lists allowed paths.
Storing comments as files seems naive, almost silly, when compared to what other comment systems do - usually store data in SQL databases. When you think about it, it makes sense for a small webpage. I doubt I have more than 20 comments on my entire website. I don’t expect much change in the pace of new comments submission. Even if this website grew, I don’t think scaling will be my issue. I cannot imagine this website accumulating 10000 comments throughout my and its lifetime. Even if it did, is 10000 files a lot? Is 10x of that a lot? Would any PC struggle with 10 x 10000 text files?
Currently, it’s extremely easy to search through comments using grep
, batch modify them and
to back them up, by just archiving all the directories. There’s no dependency on any database
technology in the entire project. That contributes to the implementation being extremely simple.
It’s also very easy to censor bad words in comments. It’s very simple to add a function which
analyzes the text and replaces bad words, before saving it into a file or after reading it from a
file. It’s also extremely simple to edit comments manually. Yes, that means I can just change your
comment whenever I want… this system isn’t unique in that way, it just makes it slightly easier.
Lets go back to HTMX. I would like to underline the main benefits of using it. The first one is
simple: it’s easy. The second one is more interesting and it goes back to the essence of HTMX
and REST API. Notice that the client receives the HTML and replaces the entire div
. It isn’t
interested in what it receives. I can continue developing the server side components, without
touching the website - the previously discussed code snippet stays the same.
It might become a bit more clear if we look at the server’s response (it’s still work in progress):
<div id="comments">
<div class="comments">
<div class="comment">
<div class="comment-meta">
<div class="comment-author">
<span>michal</span>,
<span>michal@gmail.com</span>
</div>
<div class="comment-date">
<span>2023-08-08</span>
<span>09</span><span>38</span><span class="comment-date-seconds">53</span>
</div>
</div>
<div class="comment-content">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</p>
<p>
Ut in tortor eu odio tincidunt volutpat vel et augue.
</p>
</div>
</div>
</div>
<div class="comment-submit">
<form hx-post="http://127.0.0.1:24000/naive" hx-vals='{"for": "example" }' hx-target="#comments" enctype="multipart/form-data">
<input type="text" id="comment_author" name="comment_author" placeholder="Name" required><br>
<input type="text" id="comment_contact" name="comment_contact" placeholder="e-mail or other contact info"><br>
<textarea id="comment" name="comment" placeholder="Comment..." required></textarea><br>
<button id="submit" class="custom-file-upload" type="submit">Submit</button>
</form>
</div>
</div>
The server responds with an HTML block. The div
of class="comments"
lists all the comments
for this webpage. In this case it’s only one comment, with comment-content
of two dummy
paragraphs. The div
of class="comment-submit"
implements the form one can use to submit a new
comment. When you press Submit
button it’ll send a POST (notice hx-post
) request, with the
fields comment_author
, comment_contact
and comment
. Once again I’m using the hx-vals
to
append the URL parameters. The form
element uses the hx-target-"#comments"
attribute. That’s
because as soon as the server receives the POST request, it creates a new comment and then sends
the comments HTML, including the new one, back to the client. The form shouldn’t target self for
what should be replaced with the response. The entire <div id="comments">
DOM element gets
replaced, with the updated list of comments. There’s space for improvement here because the only
thing that the server should return is the correctly HTML formatted comment the user just
submitted. All other data the browser already has.
I hope it’s clear, from this example, that HTMX gives you the ability to focus on the server side implementation. You can have a single entry point - for me that’s the first code snippet, included in my website’s code - through which you extend your website’s functionality. There’s something exciting about this approach. Today my server returns a list of comments with a submit form. Tomorrow you might visit my website and it might return the same list but the submit form will have a checkbox, that if checked, makes it so that I receive the e-mail you have inputted, but it doesn’t show up on the comments list, because you prefer to now share it publicly. The functionality grows in an organic form.
One thing that doesn’t fully compute in my head is the CSS. The server can send a response with a default CSS styling, but if you want to integrate the response HTML neatly, you have to style it accordingly. To a certain extent you can do it through semantic elements, but when you want to style a specific class of elements, then you need to have the a priori knowledge of those classes - know their names and their purpose.
No one ever promised us the client can be completely ignorant about the server’s response and it’s definitely a great feature of HTML and CSS that two, completely different, websites can pull the same data and present it in its own style.
Maybe the missing CSS, CSS from the HTMX creator, addresses this? It markets itself as:
The classless-ish CSS library you’ve been missing
One day I’d like to replace the syntax highlighting, with my own solution.
I like the naivete of this project. It’s quite easy to find a pre-made solution, to most of the technical problems. There’s many comment systems to chose from and they are full of features my comment system will never have. They’re also full of dependencies and “smart” solutions. The authors usually provide a Docker image file, because it’s just too difficult to boot up that system, without a self contained environment. This project will never have a Dockerfile. Not as a “fuck you” to the idea itself, more of a self imposed limitation: if the project needs to be containerized then it’s too complex.
Don’t be intimidated by other implementations of your idea. Those have their own issues. Your solution will have its own issues, but at least you’ll have some control over where the weak points lie.