YeenDeer softness blog (programming and electronics)

Ellie the Yeen is a soft YeenDeer that mweeoops and does programming

View on GitHub About Bots Projects Tags

Mastodon comment section on GitHub pages

So you want a comment section on your blog on Jekyll but as it is static you cannot do any server side processing at all and you have to rely on Disqus or a similar service. Another thing you can do it make your own comment section using Mastodon since it tends to have a very open API with Access-Control-Allow-Origin: * which means that JavaScript in a browser can access the data. This is something that has been done by quite a few like here and here so it is not an entirely new concept but it is a useful one.

So what do we need in order to make such a comment section. The first is we need a Jekyll site, a Mastodon account with an open enough API and some kind of service to connect them together like you can use GitHub Actions to do this but it has a few issue like if you then store the id in the same repository then it might interrupt the pages build and send you an email.

There are a few issues that can come up if you try to post with actions as you do not know what will finish first of the pages build and the posting script. If the posting script posts first then it links a non existing posts where it needs to be posted which breaks the preview and fast users might get a 404. The pages build should optimally finish first but there is no reliable way I have managed to get this to work.

What we do to fix this chicken and egg problem is involve a third thing which is a gist that stores the blog ids and the mastodon ids together so they can be fetched easily for usage in the comment section. Once the gist is fetched we can check what mastodon id corresponds to the post id and fetch the comments from it.

We are using an external thing for this right now even tho GitHub actions could technically be used where we use GitHub hooks. We start with defining a rule in IOTReact that will run when we receive a GitHub hook JSON payload.

Part of iotreact/

def githubapphook(c, p, m, redis):
        j = json.loads(m)
    except Exception:
        redis.publish("boterror", f'JSON object not decodable {m!a}')
    action = j.get("action", "push")
    if "repository" not in j or "name" not in j["repository"]:
        redis.publish("junk", f"Github App User {action}")
    repo = j["repository"]["name"]
    redis.publish("junk", f"Github App Repo {repo} {action}")
    if repo == '' and action == 'completed':
        debug = 0
        def fetchblogrss():
            global blogrsstask
            blogrsstask = None
            if debug: redis.publish('junk', 'finally posting')
            os.system("/home/pi/bots/posters/botpost/ rss")
            if debug: redis.publish('junk', 'finally posted')

        global blogrsstask
            if blogrsstask:
                if debug: redis.publish('junk', 'Rescheduling')
        except ValueError:
           blogrsstask = None
           #if debug: redis.publish('junk', 'Has already run. Will not reschedule')
        except NameError:

        blogrsstask = scheduler.enter(60, 1, fetchblogrss)

As you see it waits until 60 seconds after the last complete message has been sent for the repository to prevent race conditions. It then runs a script that runs a RSS posting script through an error handler to be safe which I use for all my bots to log crashes and problems. I also had to write a simple scheduler in IOTReact for this which is shown below.

Part of iotreact/

import threading, time, sched

except Exception:
    scheduler = sched.scheduler(time.monotonic, time.sleep)
    def schfunc():
        while True:
            except Exception:
                import redis
                redis.Redis().publish('boterror', f"IOReact Scheduler\n{traceback.format_exc()}")

    schedthread = threading.Thread(target=schfunc)
    schedthread.daemon = True

The script that is started by this is a RSS poster that has quite a bit of new features since last posted about. It does a whole bunch of things like posts on Mastodon, updates the gist, then posts in a Discord channel and finally updated the index in IndexNow. Below is the script that does all these things including parsing the RSS feed that is created by jekyll-feed.

Part of rss/

def handleblogfeed(doc: bs):
    for d in"entry"):
        title ="title")[0].text
        url ='link[href^="http"][href$=".html"]')[0].attrs["href"]
        # url ='id')[0].text
        slug = url.split("/", 3)[-1].replace("/", "-").rsplit(".", 1)[0]
        if slug in posts:
        # Post on Mastodon
        posttext = f"New blog post: {title} {url}"
        mast = getmast()
        mastpost = mast.status_post(status=posttext)
        appendpost(slug, mastpost["id"])
        # Update the Mastodon gist
        # Publish on Discord
        import redis
        r = redis.Redis()
        r.publish("discord.cin.1170179069212631121", f"{url}\n{title}")
        # Publish to IndexNow
        a = {
            "host": "",
            "key": "ead23039227a4156b16a573eb69c5981",
            "keyLocation": "",
            "urlList": [url],
        r ="", json=a)

This is what is started by the RSS poster and it updates the gist with the ids and matches them together.

cd "$(dirname "$0")"
#setopt verbose
#cp posts.csv gistrepo/posts.csv
cd gistrepo
git commit -a -m "$(date +'%Y-%m-%d %H:%M:%S')"
git push origin main

The reason for the commented copy is that a symlink is there instead which makes it easier.

Next we have the gist we need to use somehow. Which is here
We might get some URL like this when we click raw
But we can correct it like this to always get the latest version
and now we have a gist that is gradually updated and used to store publicly accessible data.

The current content of the gist looks like the following


The Mastodon ids are first for easier formatting and readability.

What we have next is a giant mess that actually fetches the gist, fetches the comments and renders them on the page when a button is pressed.
Part of _includes/comments.hhtml

var thisid = "{{ | slice: 1, 999 | replace: '/', '-' }}"
var load_mastodon = function () {
        .then((d) => d.text())
        .then((t) => {
            var thisarticle = null;
            var lines = t.split("\n");
            for (var i = 0; i < lines.length; i++) {
                var a = lines[i].split(",")
                if (a[1] === thisid) {
                    thisarticle = a[0];
            var info = document.getElementById("mastsinfobox");
            if (thisarticle === null) {
                info.textContent = "Sorry we could not find the post. It might not have been posted yet"
            var a = document.createElement("a");
            a.href = `${thisarticle}`;
   = "_blank"
            a.text = "Reply to this post to add a comment"
            info.textContent = "Loading comments"
                .catch((e) => document.getElementById("mastodon_thread").textContent = e)
                .then((d) => d.json())
                .then((j) => {
                    if (!j.descendants) {
                        info.textContent = "No comments"
                    var elem = document.getElementById("mastcomments");
                    var i = 0;
                    for (var a of j.descendants) {
                        var comm = document.createElement("div")
                        var useri = document.createElement("div")
                        var pfp = document.createElement("img")
                        pfp.src = a.account.avatar
                        pfp.width = 100
                        pfp.height = 100
                        var name = document.createElement("div")
                        var uurl = document.createElement("a")
               = "_blank"
                        uurl.href = a.url;
                        uurl.textContent = `${a.account.display_name} (${a.account.username})`;
                        var texte = document.createElement("div")
                        texte.innerHTML = a.content
                    info.textContent = `${i} comments`
                .catch((e) => info.textContent = e)

It uses a Liquid template from Jekyll too as you see to get the proper id for the article to match it.

What comes next was a giant pain as I am really not good at HTML and CSS but I managed to get a somewhat good format for the comments using the following CSS

#mastcomments {
    display: grid;
    gap: 10px;

.mastcomment {
    background-color: #222222;
    border-radius: 5px;
    padding: 5px;
    margin-top: 5px;
    padding-bottom: 15px;
    display: grid;
    grid-column: 1;
    grid-template-columns: 100px auto;
    gap: 10px;

.mastcommenttext {
    margin-left: 10px;
    background-color: #111111;
    border-radius: 5px;
    padding: 15px;
    padding-top: 5px;
    min-height: 100px;
    margin-right: 5px;
    grid-column: 2;

.userinfo {
    width: 120px;
    grid-column: 1;

It took quite some time to figure out the whole grid layout thing and it was the most painful thing to fix in the entire project.

Feel free to read the rest of the repository for how it is made as it is public:

Anyway this was a fun project with some parts that were quite a bit of effort to get fixed like HTML and CSS as most of the other things were way easier related to programming rather than design. You can copy the code I used for this and use on your own blog but be warn that the code is a mess. There are probably way better ways to do certain things here that I did like using some libraries rather than raw JavaScript. Feel free to suggest anything fun to do or any fix in the comment section now that it is there.

You should be able to see comments below this or if not you can add one.


By ellietheyeen 14 November 2023 Permalink Tags: comments iotreact jekyll mastodon python sched