Running a routing performance check on my blog I noticed that in the list of domains being accessed included facebook.com. Except, I don’t have anything to do with Facebook on my blog and I certainly don’t want to be adding to their tracking.

I was [rather pissed](https://mobile.twitter.com/rem/status/1138111172048248832) that my blog contributes to Facebook’s data so it was time to eject Disqus and look at the alternatives.

[I’ve published 38 videos for new developers, designers, UX, UI, product owners and anyone who needs to conquer the command line today.](https://training.leftlogic.com/buy/terminal/cli2?coupon=BLOG\&utm_source=blog\&utm_medium=banner\&utm_campaign=remysharp-discount)

Disqus is free…but not free at all[](#disqus-is-freebut-not-free-at-all)

When you’re not paying in cash, you’re paying in another way. I should have been a bit more tuned in, but it fairly obvious (now…) that Disqus' product is you and me.

As [Daniel Schildt / @autiomaa pointed out on twitter](https://mobile.twitter.com/autiomaa/status/1138118026103070720)

Disqus is even worse when you realise that their main product [data.disqus.com](http://data.disqus.com) is their own tracking dataset.

"The Web’s Largest First-Party Data Set"

Not cool. I do not want to be part of that, nor be forcing my unwitting reader to become part of that. So, what are the options?

Options[](#options)

I’d already been window shopping for an alternative commenting system. There’s [a few](https://github.com/utterance/utterances) [github](https://imsun.github.io/gitment/) based systems that rely on issues. It’s cute, but I don’t like the idea of relying so tightly on a system that really wasn’t designed for comments. That said, they may work for you.

There’s also a number of open source & self hosted options - I can’t vouch for them all, but I would have considered them as I’m able to review the code.

All of these options would be ad-free, tracking free, typically support exported Disqus comments and be open source. I personally settled on [Commento](https://commento.io) for a few reasons:

I’ve opted to take the commercial route a pay for the product for the time being. Though there’s some fine print about 50K views per month limit - and I’m hoping that’s a soft limit because although my blog sits around 40K, a popular article like my [CLI: Improved](https://remysharp.com/2018/08/23/cli-improved) kicked me into the 100K views in a month and shot the post into 1st place on my [popular page](https://remysharp.com/popular).

That said, if I do run into limits, I can [move to a self hosted](https://blog.rraghur.in/notes/commento/) version with [Scaleway](https://scaleway.com) or [DigitalOcean](https://digitalocean.com) or the like for €3-$5 a month.

The initial setup was very quick (under 60 minutes to being ready to launch) but I ran into a couple of snags which I’ve documented here.

Commento TL;DR[](#commento-tldr)

These are the snags that I ran into and fixed along the way. It may be that none of this is a problem for you, but it was for me.

  • Testing offline isn’t possible because the server reads the browser’s location

  • If disqus' comment URLs don’t match the post URLs (for any reason) the comments won’t appear

  • Ordered by oldest to newest

  • Avatars are lost in export to import

  • Performance optimisations could be done (local CSS, accessibly colour contrast, etc)

That all said and done, the comments are live, and look great now that I’m not feeding the ~Facebook~ beast.

![Comments](/images/new-comments.jpg)

Testing Commento offline & adjusting urls[](#testing-commento-offline—​adjusting-urls)

The commento.js script will read the location object for the host and the path to the comments. This presents two problems:

  1. Testing offline isn’t possible as the host is localhost

  2. When I migrated from one platform to another (back from wordpress to my own code), my URLs [dropped the slash](https://remysharp.com/2019/03/25/slashed-uri) - this means that no comments are found for the page

The solution is to trick the Commento script. The JavaScript is reading a parent object - this is another name that the global window lives under (along with top, self and depending on the context, this).

So we’ll override the parent:

window.parent = {
  location: {
    host: 'remysharp.com',
    pathname: '/2019/04/04/how-i-failed-the-a/'
  }
};

Now when the commento.js script runs, both works locally for testing, but also loads the right path (in my case I’m actually using a variable for everything right before the trailing /).

Caveat: messing with the parent value at the global scope could mess with other libraries. Hopefully that don’t rely on these globals anyway.

Alternative self hosted solution[](#alternative-self-hosted-solution)

Another methods is to download the open source [version of commento.js](https://gitlab.com/commento/commento/blob/master/frontend/js/commento.js) front end library and make a few changes - which is what I’ve needed to do in the end.

Firstly, I created two new variables: var DOMAIN, PATH and when the code read the data-* attributes off the script, I also support reading the domain and reading the page:

noFonts = attrGet(scripts[i], 'data-no-fonts');
DOMAIN = attrGet(scripts[i], 'data-domain') || parent.location.host;
PATH = attrGet(scripts[i], 'data-path') || parent.location.pathname;

I’m using the commento.js script with data attributes, which in the long run, is probably safer than messing with parent:

<script defer async
  data-path="/2019/04/04/how-i-failed-the-a/"
  data-domain="remysharp.com"
  src="/js/commento.js"></script>

It’s important that the self-hosted version lives in /js/commento.js as it’s hard coded in the commento.js file.

Ordered by most recent comment[](#ordered-by-most-recent-comment)

By default, Commento shows the first comment first, so the newest comments are at the end. I prefer most recent at the top.

With the help of flex box and some nice selectors, I can reverse all the comments and their sub-comments using:

#commento-comments-area > div,
div[id^="commento-comment-children-"] {
  display: flex;
  flex-direction: column-reverse;
}

Preserving avatars[](#preserving-avatars)

During the import process from disqus to commento, the avatars are lost. Avatars add a nice feeling of ownership and a reminder that there’s (usually) a human behind the comment, so I wanted to bring these back. This process is a little more involved though.

This required a little extra mile. The first step is to capture all the avatars from disqus and upload them to my own server. Using the exported disqus XML file, I’m going to grep for the username and real name, download the avatar from disqus [using their API](https://disqus.com/api/docs/images/) and save the filename under the real name.

I have to save under the real name as that’s the only value that’s exposed by commento (though in the longer run, I could self-host commento and update the database avatar field accordingly). It’s a bit gnarly, but it works.

This can all be done in single line of execution joining up unix tools:

$ grep '<username>' remysharp-2019-06-10T16:15:12.407186-all.xml -B 2 |
  egrep -v '^--$' |
  paste -d' ' - - -  |
  sort |
  uniq |
  grep -v "'" |
  awk -F'[<>]' '{ print "wget -q https://disqus.com/api/users/avatars/" $11 ".jpg -O \\\"" $3 ".jpg\\\" &" }' |
  xargs -I CMD sh -c CMD

You might get away with a copy and paste, but it’s worth explaining what’s going on at each stage in case it goes wrong so hopefully you’re able to adjust if you want to follow my lead. Or if that worked, you can [skip to the JavaScript](#javascript-to-load-these-avatars) to load these avatars.

How the combined commands work[](#how-the-combined-commands-work)

In little steps:

grep '<username>' {file} -B 2[](#grep-username-file—​b-2)

Find the instance of <username> but include the 2 previous lines (which will catch the user’s name too).

egrep -v '^--$'[](#egrep—​v---)

When using -B in grep, it’ll separate the matches with a single line of --, which we don’t want, so this line removes it. egrep is a "regexp grep" and -v means remove matches, then I’m using a pattern "line starts with - and ends with another -".

paste -d' ' - - -[](#paste—​d-------)

This will join lines (determined by the number of -`s I use) and join them using the delimiter ’ ' (space).

sort | uniq[](#sort—​uniq)

When getting unique lines, you have to sort first.

grep -v "'"[](#grep—​v-)

I’m removing names that have a dash (like O’Connel) because I couldn’t escape them in the next command and it would break the entire command. An acceptable edge case for my work.

awk -F'[<>]' …[](#awk—​f-)

This is the magic. awk will split the input line on < and > (the input looking like <name>Remy</name><isAnonymous>false</isAnonymous><username>rem</username> which came from the original grep). Then using the { print "wget …" } I’m constructing a wget command that will request the URL and save the jpeg under the user’s full name. Importantly I must wrap the name in quotes (to allow for spaces) and escape those quotes before passing to the next command.

xargs -I CMD sh -c CMD[](#xargs—​i-cmd-sh—​c-cmd)

This means "take the line from input and execute it wholesale" - which triggers (in my case, 807) wget requests as background threads.

If you want learn more about the command line, you can check out [my online course](https://terminal.training/?coupon=READERS-DISCOUNT) (which has a reader’s discount applied 😉).

The whole thing runs for a few seconds, then it’s done. In my case, I included these in my images directory on my blog, so I can access them via [avatars/rem.jpg](https://download.remysharp.com/comments/avatars/rem.jpg).

JavaScript to load these avatars[](#javascript-to-load-these-avatars)

Inside the commento.js file, when the commenter doesn’t have a photo, the original code will create a div, colour it and use the first letter of their name to make it look unique.

I’ve gone ahead and changed that logic so that it reads:

If there’s no photo, and the user is not anonymous, create an image tag with a data-src attribute pointing to my copy of the avatar. Then set the image source to my "no-user" avatar (I’ll come on to why in a moment) and apply the correct classes for an image.

If and only if, the image fires the error event, I then create the originally Commento element and replace the failed image with the div.

Then, once Commento has finished loading, I apply an IntersectionObserver to load as required (rather than hammering my visitors network with avatar images that they may never scroll to) thanks to [Zach Leat’s tip this week](https://www.zachleat.com/web/facepile/).

avatar = create('img');
avatar.setAttribute(
  'data-src',
  `https://download.remysharp.com/comments/avatars/${
    commenter.name
  }.jpg`
);
classAdd(avatar, 'avatar-img');
avatar.src = '/images/no-user.svg';
avatar.onerror = () => {
  var div = create('div');
  div.style['background'] = color;
  div.innerHTML = commenter.name[0].toUpperCase();
  classAdd(div, 'avatar');
  avatar.parentNode.replaceChild(div, avatar);
};

As I mentioned before, I’m using the IntersectionObserver API to track when the avatars are in the viewport, then the real image is loaded - reducing the toll on my visitor. However, I can only apply the observer once the images exist in the DOM.

To do this I need to [configure](https://docs.commento.io/configuration/frontend/#configuration-settings) Commento to let me do a manual boot using the data-auto-init="false" attribute on the script tag.

Once the script is loaded, in an inline deferred script I use this bit of nasty code, that keeps checking for the commento property, and once it’s there, it’ll call the main function - which takes a callback that I’ll use to then apply my observer:

function loadCommento() {
  if (window.commento && window.commento.main) {
    window.commento.main(() => observerImages());
  } else {
    setTimeout(loadCommento, 10);
  }
}
setTimeout(loadCommento, 10);

Note that this JavaScript only ever comes after the script tag with commento.js included. However, I had to make another change to the commento.js to ensure

Accessibility and performance[](#accessibility-and-performance)

The final tweak was to get my lighthouse score up. There were a few issues with accessibility around contrast (quite probably because I use a slightly off-white background).

It didn’t take too much though (I’m going to assume you’re okay reading the nested syntax - I use [Less](http://lesscss.org/), you might use SCSS, if not, remember to unroll the nesting):

body .commento-root {
  .commento-logged-container .commento-logout,
  .commento-card .commento-timeago,
  .commento-card .commento-score,
  .commento-markdown-button {
    color: #757575;
  }

  .commento-card .commento-option-button,
  .commento-card .commento-option-sticky,
  .commento-card .commento-option-unsticky {
    background: rgb(73, 80, 87);
  }
}

I also moved to using a local version of the CSS file, using the data-css-override attribute on the script tag. The final change I made was in commento.js to add a (empty) alt attribute on my signed in avatar and added rel=noopener on the link to [commento.io](http://commento.io) - both of which are worthwhile as pull requests to the project.


So that’s it. No more tracking from Bookface when you come to my site. Plus, you get to try out a brand new commenting system. Then at some point, I’ll address the final elephant in the room: Google Analytics…

Published 11-Jun 2019 under #web & #code. [Edit this post](https://github.com/remy/remysharp.com/blob/main/public/blog/ejecting-disqus.md)

👍 87 likes

[![BayuBayu](/images/no-user.svg "BayuBayu")](![Ms. Curd(/images/no-user.svg "Ms. Curd")](![Kyle Bradshaw(/images/no-user.svg "Kyle Bradshaw")](![Antti Riikola(/images/no-user.svg "Antti Riikola")](![Julya Buhain(/images/no-user.svg "Julya Buhain")](![Cameron Moll(/images/no-user.svg "Cameron Moll")](![Bruce Lawson, rebranding as BRUCE LAWSON(/images/no-user.svg "Bruce Lawson, rebranding as BRUCE LAWSON")](![Matt Allen(/images/no-user.svg "Matt Allen")](![Daniel Schildt(/images/no-user.svg "Daniel Schildt")](![some call me mattp(/images/no-user.svg "some call me mattp")](![JonyBebo(/images/no-user.svg "JonyBebo")](![smokinCoffee(/images/no-user.svg "smokinCoffee")](![Pierre-Gilles Leymarie ✈️(/images/no-user.svg "Pierre-Gilles Leymarie ✈️")](![Becky Lingafelter(/images/no-user.svg "Becky Lingafelter")](![Jon Barlow(/images/no-user.svg "Jon Barlow")](![Matt Biilmann(/images/no-user.svg "Matt Biilmann")](![Anthony L(/images/no-user.svg "Anthony L")](![Bart Vandeputte(/images/no-user.svg "Bart Vandeputte")](![Konrad Dzwinel(/images/no-user.svg "Konrad Dzwinel")](![ChillTyme(/images/no-user.svg "ChillTyme")](![Emmanuel Vuigner(/images/no-user.svg "Emmanuel Vuigner")](![Uwe Trenkner(/images/no-user.svg "Uwe Trenkner")](![Sriram Velamur (SMV)(/images/no-user.svg "Sriram Velamur (SMV)")](![James Kenny(/images/no-user.svg "James Kenny")](![Victor M(/images/no-user.svg "Victor M")](![Lise Kemen(/images/no-user.svg "Lise Kemen")](![Guillaume Mouron(/images/no-user.svg "Guillaume Mouron")](![Patrick Connors(/images/no-user.svg "Patrick Connors")](![Jatesadakarn(/images/no-user.svg "Jatesadakarn")](![Asdrúbal Iván 🇻🇪(/images/no-user.svg "Asdrúbal Iván 🇻🇪")](![Markolas(/images/no-user.svg "Markolas")](![Martin Akolo Chiteri(/images/no-user.svg "Martin Akolo Chiteri")](![Olivier Forget(/images/no-user.svg "Olivier Forget")](![Fredrik Andersson(/images/no-user.svg "Fredrik Andersson")](![Blessing Richardson(/images/no-user.svg "Blessing Richardson")](![Zander(/images/no-user.svg "Zander")](![\_ michael(/images/no-user.svg "_ michael")](![✨ Joël(/images/no-user.svg "✨ Joël")](![Peter Grucza(/images/no-user.svg "Peter Grucza")](![Jason Cartwright(/images/no-user.svg "Jason Cartwright")](![Dicky Ndiaye Johnson(/images/no-user.svg "Dicky Ndiaye Johnson")](![Jens Grochtdreis(/images/no-user.svg "Jens Grochtdreis")](![Stuart Clarke-Frisby(/images/no-user.svg "Stuart Clarke-Frisby")](![Haroen(/images/no-user.svg "Haroen")](![David Pich(/images/no-user.svg "David Pich")](![Simon Willison(/images/no-user.svg "Simon Willison")](![Vlad Știrbu(/images/no-user.svg "Vlad Știrbu")](![let name =(/images/no-user.svg "let name =")](![Benjamin Listwon(/images/no-user.svg "Benjamin Listwon")](![Benoît Burgener(/images/no-user.svg "Benoît Burgener")](![Nicholas Fazzolari(/images/no-user.svg "Nicholas Fazzolari")](![Article Seven(/images/no-user.svg "Article Seven")](![Andrew Knox(/images/no-user.svg "Andrew Knox")](![James Moberg(/images/no-user.svg "James Moberg")](![Loïc BOURG(/images/no-user.svg "Loïc BOURG")](![(/images/no-user.svg "")](![François(/images/no-user.svg "François")](![Seb 🌱(/images/no-user.svg "Seb 🌱")](![YannPicarddeMuller(/images/no-user.svg "YannPicarddeMuller")](![Ahmad Shadeed(/images/no-user.svg "Ahmad Shadeed")](![Bennett Feely(/images/no-user.svg "Bennett Feely")](![Rick Yentzer(/images/no-user.svg "Rick Yentzer")](![Adam-A zATE(/images/no-user.svg "Adam-A zATE")](![Alexis La Porte(/images/no-user.svg "Alexis La Porte")](![florin cosmin onciu(/images/no-user.svg "florin cosmin onciu")](![Simon Georges(/images/no-user.svg "Simon Georges")](![Colm Delaney(/images/no-user.svg "Colm Delaney")](![Ana Rodrigues(/images/no-user.svg "Ana Rodrigues")](![Rick Butterfield(/images/no-user.svg "Rick Butterfield")](![Jewel Barnett, Ph.D.(/images/no-user.svg "Jewel Barnett, Ph.D.")](![Nicolas Hoizey(/images/no-user.svg "Nicolas Hoizey")](![Edgar Barrantes(/images/no-user.svg "Edgar Barrantes")](![Aleks Hudochenkov(/images/no-user.svg "Aleks Hudochenkov")](![José Bellorín(/images/no-user.svg "José Bellorín")](![Tahir Khalid(/images/no-user.svg "Tahir Khalid")](![Arek(/images/no-user.svg "Arek")](![Clayton Miller(/images/no-user.svg "Clayton Miller")](![Rendall(/images/no-user.svg "Rendall")](![Yavorski(/images/no-user.svg "Yavorski")](![ripter001(/images/no-user.svg "ripter001")](![𝕨𝕚𝕝𝕝 𝕜𝕒𝕞𝕠𝕧𝕚𝕥𝕔𝕙(/images/no-user.svg "𝕨𝕚𝕝𝕝 𝕜𝕒𝕞𝕠𝕧𝕚𝕥𝕔𝕙")](![Zach Leatherman(/images/no-user.svg "Zach Leatherman")](![Dave Rupert(/images/no-user.svg "Dave Rupert")](![Ben Hutton(/images/no-user.svg "Ben Hutton")](![Nils 'Das ist aber euer letztes Kind, oder?' Hitze(/images/no-user.svg "Nils 'Das ist aber euer letztes Kind, oder?' Hitze")](![Chris Enns 👨🏻‍💻(/images/no-user.svg "Chris Enns 👨🏻‍💻")](![𝔻𝕒𝕧 ℂ𝕙𝕒𝕟𝕒(/images/no-user.svg "𝔻𝕒𝕧 ℂ𝕙𝕒𝕟𝕒")](https://twitter.com/davchana)

Comments

Lock Thread

Login

Add Comment[M ↓   Markdown]()

[Upvotes]()[Newest]()[Oldest]()

Nitesh Manav

0 points

3 years ago

Loved this article ❤️ Bookmarking it so that I can later use it when I actually want to try Commento

![](/images/no-user.svg)

KAI

0 points

4 years ago

This is great. Been looking for a Disqus alternative.

0 points

4 years ago

UPDATE: I’ve now written a followup blog post detailing some of the pains of self-hosting and migration: https://www.davidbcalhoun.com/2020/ditching-disqus-migrating-away-since-it-has-become-a-monster/

Thanks Remy! I’m not sure yet if this is the best solution for me, but I’ve gone ahead and tried the local version of Commento, running on a free tier Amazon EC2 instance (with Docker). I’m hoping the cost of the free tier instance will be less than the minimum Commento $5/month fee (hey, I’m cheap and also currently not employed, so I’ve got to be creative here!).Some downsides I’ve found so far with the migration from Disqus:

  • No built-in support for SSL when self-hosting. This is a big downside, as it requires extra time fiddling around setting up a proxy

  • In addition to losing avatars, you also lose post votes (both are not exported by Disqus unfortunately). This info does appear in the Disqus API responses in its Moderate page, so it should just be a matter of writing a script to map that JSON to some Postgres upsert\* If your Disqus export doesn’t contain trailing slashes and your website does, there will be a mismatch and the page won’t be found in Postgres. This was a bit of a time sink for me. I hope they can make this agnostic in the future.

  • Spaces seem to get eaten up in comments when trying to edit them? Some Markdown issue?

  • (minor) Moderation panel lives on each individual page on your own website. There’s no way to see an overview of all comments (the Commento settings page is a bit sparse)

  • (minor) Relative time formatting is a little funky. Commento displays "24 months ago" instead of a more natural "2 years ago"

0 points

4 years ago

This looks great. I think i will try it on one of my self hosted domains soon. Thanks Remy.

![](/images/no-user.svg)

Gil Romano

0 points

4 years ago

Does the hosted version of commento provide any options for exporting and/or backing up the comments? I couldn’t find anything to that effect on commento.io

![](/images/no-user.svg)

Gil Romano

1 point

4 years ago

Found the answer to my own question. A search in the docs for "backup" or "export" yields nothing, but once you create an account and end up at the dashboard, go to settings/general/export. That tab says: "You can export an archive of this domain’s data (which includes all comments and commenters) in the JSON format. To initiate and queue an archive request, click the button below. You will receive an email containing the archive once it’s ready."

![](/images/no-user.svg)

Eric Berry

2 points

4 years ago

This is fantastic. We are moving our blog away from Disqus to Commento.

![](/images/no-user.svg)

Vince Aggrippino

0 points

4 years ago

I seriously don’t see the benefit of blog comments anyway. A blog is a soap-box, a creative outlet, a medium for disseminating information, educating, or even venting frustration. None of these purposes need community commentary and they’re all better off without it.\ Don’t we already have too many ways to speak our minds to the whole internet, anyway?\ Books, newspapers, academic journals, and TV all do quite well without comments. YouTube would clearly be better without comments.\ Your life would be less stressful and more productive without feedback from unknown, random people off the internet (like me) and I’ll bet the same holds true for many would-be commenters.

2 points

4 years ago

I have to admit it’s certainly crossed my mind, but this post was about moving 13 years worth of blogging software to a new comments system.

And for my own place, I do want to welcome discord - but also a bit of narcissistism through seeing people wanting to contribute 😀

![](/images/no-user.svg)

Thibaut Allender

0 points

4 years ago

Did you think about using https://github.com/tessalt/echo-chamber-js ;-)

![](/images/no-user.svg)[Chang](https://github.com/chhuang)

0 points

4 years ago

Can the comments be server-side rendered (SEO friendly)?

0 points

4 years ago

I believe it would be possible, yes. If you look at the network requests on this page, you’ll see one going to /list at commento.io - the POST data is pretty straight forward to follow.

I think it would be fine for a smaller site, but my site has nearly 500 blog posts and I’d not keen on the idea of hitting commito.io 500 times each time I run a build.

I’ve got a few ideas how to get around this, but it needs some more exploration.

![](/images/no-user.svg)

Michel Edighoffer

1 point

4 years ago

With the free version, it’s not possible. With premium, may be yes, API is accessible.

![](/images/no-user.svg)[Simon Willison](https://github.com/simonw)

0 points

4 years ago

I really like the data- trick you are using on those script elements.

0 points

4 years ago

It’s not my own trick, but I’ve used it in the past myself (long…long ago). Though it was back in the days you could rely on lastChild during the DOM render and I’d [slurp up the query string](https://github.com/remy/jsconsole/blob/f62697444460dbe1fc9e46bd22df3e20016f5bbe/remote-client.js#L64-L68) in the script and read variables off that. Super brittle, but worked back then too :)

[Commento](https://commento.io)