Adding comments to a static blog with Mastodon
carlschwan.eu
carlschwan.eu
The nice thing about this is I get a breadth of methods for receiving comments/reactions across multiple platforms, including anything that directly supports webmention (such as https://micro.blog/, where my posts are also syndicated), without having to do any of the platform-specific wiring myself.
That I ended up with direct webmention support was just a bonus as far as I'm concerned.
Then when people react (reply, like, etc) it'll proxy those interactions back as webmentions.
If I had a real twitter following, though, I might have a different answer.
It makes sense for a coding blog since you can assume most of your users have a GitHub account.
A user could comment using any fediverse account, including Pleroma, Friendica, PixelFed, etc. Summing that up with 'mastodon account' is damaging to the whole idea of the fediverse. Non-technical people might not realize that they can choose to use the other platforms.
Also, I hate that the mastodon API has become the de facto standard for interacting with any fediverse platform. The other platforms implemented the API to maintain compatibility with mastodon clients. Now this not-quite-standard API is controlled by a single person who doesn't care about compatibility with other platforms.
document.getElementById('mastodon-comments-list').innerHTML =
data['descendants'].reduce(function(prev, reply) {
mastodonComment = `
...
`;
return prev + DOMPurify.sanitize(mastodonComment);
}, '');
Why is it implemented like this? It looks quadratic, while it's trivial (and more intuitive IMO) to make it linear: document.getElementById('mastodon-comments-list').innerHTML =
data['descendants'].map(function(reply) {
return DOMPurify.sanitize(`
...
`);
}).join(''); let root = document.getElementById('mastodon-comments-list');
data['descendants'].forEach(reply => {
let div = document.createElement('div');
div.innerHtml = DOMPurify.sanitize( ... );
root.append(div);
});Intermediate string length doesn't matter for time complexity here. If they are short they will be fast. If they are long, the time to scan and parse in DOMPurify and innerHTML will dominate.
After building the DOM, your extra divs are processed every time that section of the DOM is styled and rendered, not just once. If the number of extra divs is low enough that this is negligible, so is the number of intermediate strings in the alternative code.
So I wouldn't assume your version is faster at setting up, and it may be marginally slower later on repeated renderings. I'd profile both versions, maybe with Browserscope (which probably has a test for this particular question already).
However if I couldn't profile and was asked my guess at the fastest version, my guess is the string-join version. I'd be more concerned with whether concatenating DOMPurify.sanitize() strings is guaranteed to maintain the same security properties.
They could also probably just html-escape the few foreign values rather than bringing an HTML parser to the entire template.
In this example, updating the DOM as soon as you get the information is not optimal for user feedback, because rendering is paused until after all the DOM updates anyway. DOM rendering in all modern browsers is lazy. It only starts after JavaScript returns from handling the current event queue.
return prev + DOMPurify.sanitize(mastodonComment);
to end up behaving like this: reduceValue = reduceValue + DOMPurify.sanitize(mastodonComment);
which ends up behaving like this: reduceValue += DOMPurify.sanitize(mastodonComment);
which ends up being a sequence of in-place string appends.In any good implementation, a sequence of repeated appends takes linear time in the total length of all the appended strings, like join('').
I've just tested this theory in Safari 14.0.2 with a JS console one-liner. On this browser, both versions take linear time not quadratic, and the test corresponding to the first version even runs slightly faster.
The time difference is small though, and you're right, the second version is explicitly linear time. If I had tested with a much older browser (maybe IE8), I think the first version would have taken quadratic time.
Sometimes, a hacky solution is just enough ;)
I'm from a region where they're pretty common on news sites, so I've created a Firefox add-on that hides the comments from about 40 local websites that I chose via Alexa ranks.
I personally never read comments on blogs and I find having a "Contact Me" link at the bottom is better. On my blog I use a gmail account and a template for new articles so I get `[Contact Me](emailto:my.email+shorttitle@gmail.com)` so they're easy to categorize.
But yeah these days you could technically have a reddit thread on your user page for each of your blog posts.
But how is that different than mastodon? At least you can self-host mastodon.
Or at least harder than it should be.
I'd be happy to be proven wrong btw.
EDIT: don't get me wrong, this is cool :)
So the author is proposing tooting a link to the blog post article, and the leveraging your server’s REST endpoints to retrieve the comments.
This actually seems quite powerful, and elegant to me. If you run your own server, this could be a very nice & simple solution.
- Added reddit and twitter attributes on the frontmatter
- After publishing and sharing to both services, I paste reddit link/tweet id to the frontmatter and rebuild
- On the blogs footer, I just embed both posts, so people can see how many replies, and also click to be redirected reply on each respective service.
Although I would love something more automated (and preferably with in-site response), at least this is a good flow for the readers.
As of now, my only visitors are a few anime bloggers from my circle, all of them using WordPress. As it's not possible to use WordPress comments, I'm not losing anything besides that. I could use Disqus, but I hate it, and I think they would log-in with Twitter to leave a Disqus comment so...
As for embedding, I use Hugo's Twitter shortcode, and reddit standard embed code - as all embed are from my own subreddit, I left that hardcoded, with a placeholder title, and just change the url on the code.
Seems that Twitter embed changed recently to not show number of replies anymore, but that's alright for now. They have to click on the embedded tweet and go to twitter to reply anyway :/
I might add the webmentions thing mentioned in other comments here to show the actual replies.
This is how it looks right now: https://geekosaur.com/post/anime-controversies-controversed/
1. The use of DOMPurify is either unnecessary or insufficient. Specifically consider reply.content, which is provided from the server as HTML. (I believe it’s the only one that’s supposed to be serialised HTML; all the other fields are text.) If the backend guarantees that you get a clean DOM, DOMPurify is unnecessary†. But if it’s possible for a user to control the markup in that field completely, then DOMPurify as configured is insufficient, because although it blocks XSS and JavaScript execution, it doesn’t filter out remote resource loading and CSS, which can be almost as harmful. Trivial example, <a href=//malicious.example style=position:fixed;inset:0>pwned</a>. Given the type of content, you probably want to either blacklist quite a few things (e.g. the style attribute, and the img, picture, source, audio and video tags), or whitelist a small set of things.
2. Various fields that could hypothetically contain magic characters are dropped in with no escaping. If they do contain magic characters like < and &, you’ve just messed up the display and opened the way for malicious resource loading and problem № 3 below. Even if they are supposed to be unable to contain a magic character (e.g. I’m going to guess that reply.account.username is safe), it’s probably a good idea to escape them anyway just in case (perhaps an API change later makes it possible), and to guard against errant copy–pasters and editors of the code that don’t know what they’re doing. Perhaps at some point you’ll add or switch to reply.account.display_name, which probably can contain < and &.
3. The markup is produced by mixing static templating with user-provided input, and sanitisation is performed on the whole thing. It’s important when doing this sort of templating that each user-provided input be escaped or sanitised by itself in isolation, not as part of a whole that has been concatenated. Otherwise you can mess with the DOM tree accidentally or deliberately. Suppose, for example, that reply.content could contain `</div></div></div><img src=//ad.example alt="Legitimate-looking in-stream ad"><div class="mastodon-comment">…<div class="content">…<div class="mastodon-comment-content">…`. So this means:
• Apply attributes and text nodes to a real DOM (e.g. `img = new Image(); img.src = reply.account.avatar_static; avatar.append(img)`), or escape them in the HTML serialisation (e.g. `<img src="${reply.account.avatar_static.replace(/&/g, "&").replace(/"/g, """)}">`).
• Do HTML sanitisation on just the user input, e.g. DOMPurify.sanitize(reply.content).
A part of my problem with the code as written is that, purely from looking at the code, I can see that there may well be various security holes. I require knowledge of the backend also before I can judge whether there are security holes. Where possible, it’s best to write the code in such a way that you know that it’s safe, or that it’s not—try not to depend on subtle things like “the particular fields that we access happen to be unable to contain angle brackets, ampersands and quotes”, because they’re fragile.
Incidentally, it would also be more efficient to run DOMPurify with the RETURN_DOM_FRAGMENT option, and append that, rather than concatenating it to a string and setting innerHTML. Saves a pointless serialisation/deserialisation round trip, and avoids any possibility of new mXSS vulnerabilities that might be discovered in the future. (I don’t really understand why DOMPurify defaults to emitting a string. I can’t remember seeing a single non-demo use of DOMPurify where the string is preferable to a DOM fragment.)
—
† Though if the server sanitised arbitrary user-provided HTML/MathML/SVG, I probably don’t trust it as much as I trust DOMPurify, for things that end up in the DOM. There are some pretty crazy subtleties in things like HTML serialisation round-tripping of HTML, SVG and MathML content. There’s fun reading in the changelogs and security patches.
It is pretty trivial to generate a new static page which includes the new comment. You do want some kind of database. A SQLite file works fine.
> Some fields don't look escaped.
hmm So the future of social media is everyone becoming software engineers huh. Good luck with those toots.