Created
June 12, 2019 13:35
-
-
Save zachleat/cda7af65f5d61666e7fa6037c3ede752 to your computer and use it in GitHub Desktop.
This webmention entry made it through sanitize-html
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"type": "entry", | |
"author": { | |
"type": "card", | |
"name": "", | |
"photo": "", | |
"url": "" | |
}, | |
"url": "https://remysharp.com/2019/06/11/ejecting-disqus", | |
"published": "2019-06-11T00:00:00", | |
"wm-received": "2019-06-11T16:22:23Z", | |
"wm-id": 624833, | |
"wm-source": "https://remysharp.com/2019/06/11/ejecting-disqus", | |
"wm-target": "https://www.zachleat.com/web/facepile/", | |
"name": "Ejecting Disqus", | |
"content": { | |
"html": "<p>Running a routing performance check on my blog I noticed that in the list of domains being accessed included <a href=\"http://facebook.com\">facebook.com</a>. 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.</p>\n<p>I was <a href=\"https://mobile.twitter.com/rem/status/1138111172048248832\">rather pissed</a> that my blog contributes to Facebook's data so it was time to eject Disqus and look at the alternatives.</p>\n\n<h2>Disqus is free…but not free at all</h2>\n<p>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.</p>\n<p>As <a href=\"https://mobile.twitter.com/autiomaa/status/1138118026103070720\">Daniel Schildt / @autiomaa pointed out on twitter</a></p>\n<blockquote>\n<p>Disqus is even worse when you realise that their main product <a href=\"http://data.disqus.com\">data.disqus.com</a> is their own tracking dataset.</p>\n<p>\"The Web's Largest First-Party Data Set\"</p>\n</blockquote>\n<p>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?</p>\n<h2>Options</h2>\n<p>I'd already been window shopping for an alternative commenting system. There's <a href=\"https://github.com/utterance/utterances\">a few</a> <a href=\"https://imsun.github.io/gitment/\">github</a> 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.</p>\n<p>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.</p>\n<ul>\n<li><a href=\"https://mouthful.dizzy.zone/\">Mouthful</a></li>\n<li><a href=\"https://schnack.cool/\">Schnack</a></li>\n<li><a href=\"https://posativ.org/isso/\">Isso</a></li>\n</ul>\n<p>All of these options would be ad-free, tracking free, typically support exported Disqus comments and be open source. I personally settled on <a href=\"https://commento.io\">Commento</a> for a few reasons:</p>\n<ul>\n<li>\"Cloud\" (ie. commercial) <em>and</em> self-hosted option</li>\n<li>\n<a href=\"https://gitlab.com/commento/commento\">Open source</a> and accepting issues and merge requests</li>\n<li>User interface was clean and polished</li>\n</ul>\n<p>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 <a href=\"https://remysharp.com/2018/08/23/cli-improved\">CLI: Improved</a> kicked me into the 100K views in a month and shot the post into 1st place on my <a href=\"https://remysharp.com/popular\">popular page</a>.</p>\n<p>That said, <em>if</em> I do run into limits, I can <a href=\"https://blog.rraghur.in/notes/commento/\">move to a self hosted</a> version with <a href=\"https://scaleway.com\">Scaleway</a> or <a href=\"https://digitalocean.com\">DigitalOcean</a> or the like for €3-$5 a month.</p>\n<p>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.</p>\n<h2>Commento TL;DR</h2>\n<p>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.</p>\n<ul>\n<li>Testing offline isn't possible because the server reads the browser's location</li>\n<li>If disqus' comment URLs don't match the post URLs (for any reason) the comments won't appear</li>\n<li>Ordered by oldest to newest</li>\n<li>Avatars are lost in export to import</li>\n<li>Performance optimisations could be done (local CSS, accessibly colour contrast, etc)</li>\n</ul>\n<p>If you want to use my <a href=\"https://github.com/remy/remysharp.com/blob/21a3413bfaf6ed5ff8ba50c787335df3879da558/public/js/commento.js\">commento.js</a> and <a href=\"https://github.com/remy/remysharp.com/blob/21a3413bfaf6ed5ff8ba50c787335df3879da558/public/css/commento.css\">commento.css</a> you're welcome to (and these are the <a href=\"https://github.com/remy/remysharp.com/blob/21a3413bfaf6ed5ff8ba50c787335df3879da558/public/css/screen.less#L1643-L1678\">contrast changes</a>).</p>\n<p>That all said and done, the comments are live, and look great now that I'm not feeding the Facebook beast.</p>\n<p><img src=\"https://remysharp.com/images/new-comments.jpg\" alt=\"Comments\" /></p>\n<h2>Testing Commento offline & adjusting urls</h2>\n<p>The commento.js script will read the <code>location</code> object for the host and the path to the comments. This presents two problems:</p>\n<ol>\n<li>Testing offline isn't possible as the host is <code>localhost</code>\n</li>\n<li>When I migrated from one platform to another (back from wordpress to my own code), my URLs <a href=\"https://remysharp.com/2019/03/25/slashed-uri\">dropped the slash</a> - this means that no comments are found for the page</li>\n</ol>\n<p>The solution is to trick the Commento script. The JavaScript is reading a <code>parent</code> object - this is another name that the global <code>window</code> lives under (along with <code>top</code>, <code>self</code> and depending on the context, <code>this</code>).</p>\n<p>So we'll override the <code>parent</code>:</p>\n<pre><code>window<span>.</span>parent <span>=</span> <span>{</span>\n location<span>:</span> <span>{</span>\n host<span>:</span> <span>'remysharp.com'</span><span>,</span>\n pathname<span>:</span> <span>'/2019/04/04/how-i-failed-the-a/'</span>\n <span>}</span>\n<span>}</span><span>;</span>\n</code></pre>\n<p>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 <code>/</code>).</p>\n<p><strong>Caveat:</strong> messing with the <code>parent</code> value at the global scope could mess with other libraries. Hopefully that don't rely on these globals anyway.</p>\n<h3>Alternative self hosted solution</h3>\n<p>Another methods is to download the open source <a href=\"https://gitlab.com/commento/commento/blob/master/frontend/js/commento.js\">version of commento.js</a> front end library and make a few changes - which is what I've needed to do in the end.</p>\n<p>Firstly, I created two new variables: <code>var DOMAIN, PATH</code> and when the code read the <code>data-*</code> attributes off the script, I also support reading the domain and reading the page:</p>\n<pre><code>noFonts <span>=</span> <span>attrGet</span><span>(</span>scripts<span>[</span>i<span>]</span><span>,</span> <span>'data-no-fonts'</span><span>)</span><span>;</span>\n<span>DOMAIN</span> <span>=</span> <span>attrGet</span><span>(</span>scripts<span>[</span>i<span>]</span><span>,</span> <span>'data-domain'</span><span>)</span> <span>||</span> parent<span>.</span>location<span>.</span>host<span>;</span>\n<span>PATH</span> <span>=</span> <span>attrGet</span><span>(</span>scripts<span>[</span>i<span>]</span><span>,</span> <span>'data-path'</span><span>)</span> <span>||</span> parent<span>.</span>location<span>.</span>pathname<span>;</span>\n</code></pre>\n<p>I'm using the commento.js script with data attributes, which in the long run, is probably safer than messing with <code>parent</code>:</p>\n<pre><code><span><span><span><</span>script</span> <span>defer</span> <span>async</span>\n <span>data-path</span><span><span>=</span><span>\"</span>/2019/04/04/how-i-failed-the-a/<span>\"</span></span>\n <span>data-domain</span><span><span>=</span><span>\"</span>remysharp.com<span>\"</span></span>\n <span>src</span><span><span>=</span><span>\"</span>/js/commento.js<span>\"</span></span><span>></span></span><span></span><span><span><span></</span>script</span><span>></span></span>\n</code></pre>\n<p>It's important that the self-hosted version lives in <code>/js/commento.js</code> as it's hard coded in the commento.js file.</p>\n<h2>Ordered by most recent comment</h2>\n<p>By default, Commento shows the first comment first, so the newest comments are at the end. I prefer most recent at the top.</p>\n<p>With the help of flex box and some nice selectors, I can reverse all the comments and their sub-comments using:</p>\n<pre><code><span>#commento-comments-area > div,\ndiv[id^=\"commento-comment-children-\"]</span> <span>{</span>\n <span>display</span><span>:</span> flex<span>;</span>\n <span>flex-direction</span><span>:</span> column-reverse<span>;</span>\n<span>}</span>\n</code></pre>\n<h2>Preserving avatars</h2>\n<p>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.</p>\n<p>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 <a href=\"https://disqus.com/api/docs/images/\">using their API</a> and save the filename under the real name.</p>\n<p>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.</p>\n<p>This can all be done in single line of execution joining up unix tools:</p>\n<pre><code>$ <span>grep</span> <span>'<username>'</span> remysharp-2019-06-10T16:15:12.407186-all.xml -B 2 <span>|</span>\n <span>egrep</span> -v <span>'^--$'</span> <span>|</span>\n <span>paste</span> -d<span>' '</span> - - - <span>|</span>\n <span>sort</span> <span>|</span>\n <span>uniq</span> <span>|</span>\n <span>grep</span> -v <span>\"'\"</span> <span>|</span>\n <span>awk</span> -F<span>'[<>]'</span> <span>'{ print \"wget -q https://disqus.com/api/users/avatars/\" <span>$11</span> \".jpg -O \\\\\\\"\" <span>$3</span> \".jpg\\\\\\\" &\" }'</span> <span>|</span>\n <span>xargs</span> -I CMD sh -c CMD\n</code></pre>\n<p>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 <a href=\"https://remysharp.com/2019/06/11/ejecting-disqus#javascript-to-load-these-avatars\">skip to the JavaScript</a> to load these avatars.</p>\n<h3>How the combined commands work</h3>\n<p>In little steps:</p>\n<h4><code>grep '<username>' {file} -B 2</code></h4>\n<p>Find the instance of <code><username></code> but include the 2 previous lines (which will catch the user's name too).</p>\n<h4><code>egrep -v '^--$'</code></h4>\n<p>When using <code>-B</code> in grep, it'll separate the matches with a single line of <code>--</code>, which we don't want, so this line removes it. <code>egrep</code> is a \"regexp grep\" and <code>-v</code> means remove matches, then I'm using a pattern \"line starts with - and ends with another -\".</p>\n<h4><code>paste -d' ' - - -</code></h4>\n<p>This will join lines (determined by the number of <code>-</code>s I use) and join them using the delimiter <code>' '</code> (space).</p>\n<h4><code>sort | uniq</code></h4>\n<p>When getting unique lines, you have to sort first.</p>\n<h4><code>grep -v \"'\"</code></h4>\n<p>I'm removing names that have a dash (like <code>O'Connel</code>) because I couldn't escape them in the next command and it would break the entire command. An acceptable edge case for my work.</p>\n<h4><code>awk -F'[<>]' …</code></h4>\n<p>This is the magic. <code>awk</code> will split the input line on <code><</code> and <code>></code> (the input looking like <code><name>Remy</name><isAnonymous>false</isAnonymous><username>rem</username></code> which came from the original <code>grep</code>). Then using the <code>{ print \"wget …\" }</code> I'm constructing a <code>wget</code> 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.</p>\n<h4><code>xargs -I CMD sh -c CMD</code></h4>\n<p>This means \"take the line from input and execute it wholesale\" - which triggers (in my case, 807) <code>wget</code> requests as background threads.</p>\n<p>If you want learn more about the command line, you can check out <a href=\"https://terminal.training/?coupon=READERS-DISCOUNT\">my online course</a> (which has a reader's discount applied 😉).</p>\n<p>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 <a href=\"https://download.remysharp.com/comments/avatars/rem.jpg\">https://download.remysharp.com/comments/avatars/rem.jpg</a></p>\n<h3>JavaScript to load these avatars</h3>\n<p>Inside the commento.js file, when the commenter doesn't have a photo, the original code will create a <code>div</code>, colour it and use the first letter of their name to make it look unique.</p>\n<p>I've gone ahead and changed that logic so that it reads:</p>\n<p>If there's no photo, and the user is not anonymous, create an image tag with a data-src attribute pointing to <em>my</em> 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.</p>\n<p>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.</p>\n<p>Then, once Commento has finished loading, I apply an <code>IntersectionObserver</code> to load as required (rather than hammering my visitors network with avatar images that they may never scroll to) thanks to <a href=\"https://www.zachleat.com/web/facepile/\">Zach Leat's tip this week</a>.</p>\n<pre><code>avatar <span>=</span> <span>create</span><span>(</span><span>'img'</span><span>)</span><span>;</span>\navatar<span>.</span><span>setAttribute</span><span>(</span>\n <span>'data-src'</span><span>,</span>\n <span><span>`https://download.remysharp.com/comments/avatars/</span><span><span>${</span>\n commenter<span>.</span>name\n <span>}</span></span><span>.jpg`</span></span>\n<span>)</span><span>;</span>\n<span>classAdd</span><span>(</span>avatar<span>,</span> <span>'avatar-img'</span><span>)</span><span>;</span>\navatar<span>.</span>src <span>=</span> <span>'/images/no-user.svg'</span><span>;</span>\navatar<span>.</span><span>onerror</span> <span>=</span> <span>(</span><span>)</span> <span>=></span> <span>{</span>\n <span>var</span> div <span>=</span> <span>create</span><span>(</span><span>'div'</span><span>)</span><span>;</span>\n div<span>.</span>style<span>[</span><span>'background'</span><span>]</span> <span>=</span> color<span>;</span>\n div<span>.</span>innerHTML <span>=</span> commenter<span>.</span>name<span>[</span><span>0</span><span>]</span><span>.</span><span>toUpperCase</span><span>(</span><span>)</span><span>;</span>\n <span>classAdd</span><span>(</span>div<span>,</span> <span>'avatar'</span><span>)</span><span>;</span>\n avatar<span>.</span>parentNode<span>.</span><span>replaceChild</span><span>(</span>div<span>,</span> avatar<span>)</span><span>;</span>\n<span>}</span><span>;</span>\n</code></pre>\n<p>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 <abbr title=\"Document Object Model\">DOM</abbr>.</p>\n<p>To do this I need to <a href=\"https://docs.commento.io/configuration/frontend/#configuration-settings\">configure</a> Commento to let me do a manual boot using the <code>data-auto-init=\"false\"</code> attribute on the script tag.</p>\n<p>Once the script is loaded, in an inline deferred script I use this bit of nasty code, that keeps checking for the <code>commento</code> property, and once it's there, it'll call the main function - which takes a callback that I'll use to <em>then</em> apply my observer:</p>\n<pre><code><span>function</span> <span>loadCommento</span><span>(</span><span>)</span> <span>{</span>\n <span>if</span> <span>(</span>window<span>.</span>commento <span>&&</span> window<span>.</span>commento<span>.</span>main<span>)</span> <span>{</span>\n window<span>.</span>commento<span>.</span><span>main</span><span>(</span><span>(</span><span>)</span> <span>=></span> <span>observerImages</span><span>(</span><span>)</span><span>)</span><span>;</span>\n <span>}</span> <span>else</span> <span>{</span>\n <span>setTimeout</span><span>(</span>loadCommento<span>,</span> <span>10</span><span>)</span><span>;</span>\n <span>}</span>\n<span>}</span>\n<span>setTimeout</span><span>(</span>loadCommento<span>,</span> <span>10</span><span>)</span><span>;</span>\n</code></pre>\n<p>Note that this JavaScript only ever comes after the script tag with <code>commento.js</code> included. However, I had to make <em>another</em> change to the commento.js to ensure</p>\n<h2>Accessibility and performance</h2>\n<p>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).</p>\n<p>It didn't take too much though (I'm going to assume you're okay reading the nested syntax - I use <a href=\"http://lesscss.org/\">Less</a>, you might use SCSS, if not, remember to unroll the nesting):</p>\n<pre><code><span>body .commento-root </span><span>{</span>\n <span>.commento-logged-container .commento-logout,\n .commento-card .commento-timeago,\n .commento-card .commento-score,\n .commento-markdown-button </span><span>{</span>\n <span>color</span><span>:</span> #757575<span>;</span>\n <span>}</span>\n\n <span>.commento-card .commento-option-button,\n .commento-card .commento-option-sticky,\n .commento-card .commento-option-unsticky </span><span>{</span>\n <span>background</span><span>:</span> <span>rgb</span><span>(</span>73<span>,</span> 80<span>,</span> 87<span>)</span><span>;</span>\n <span>}</span>\n<span>}</span>\n</code></pre>\n<p>I also moved to using a local version of the CSS file, using the <code>data-css-override</code> attribute on the script tag. The final change I made was in <code>commento.js</code> to add a (empty) <code>alt</code> attribute on my signed in avatar and added <code>rel=noopener</code> on the link to <a href=\"http://commento.io\">commento.io</a> - both of which are worthwhile as pull requests to the project.</p>\n\n<p>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…</p>\n\n <p>Posted <time class=\"dt-published\" datetime=\"2019-06-11 00:00:00\">11-Jun 2019</time> under web & code.</p>\n \n <p>👍 78 likes</p>\n \n<a href=\"https://twitter.com/mattpep\"><img width=\"32\" title=\"some call me mattp\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"some call me mattp\" /></a><a href=\"https://twitter.com/JonyBebo\"><img width=\"32\" title=\"JonyBebo\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"JonyBebo\" /></a><a href=\"https://twitter.com/viar\"><img width=\"32\" title=\"smokinCoffee\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"smokinCoffee\" /></a><a href=\"https://twitter.com/pierregillesl\"><img width=\"32\" title=\"Pierre-Gilles Leymarie ✈️\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Pierre-Gilles Leymarie ✈️\" /></a><a href=\"https://twitter.com/blingafe\"><img width=\"32\" title=\"Becky Lingafelter\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Becky Lingafelter\" /></a><a href=\"https://twitter.com/orviwan\"><img width=\"32\" title=\"Jon Barlow\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Jon Barlow\" /></a><a href=\"https://twitter.com/biilmann\"><img width=\"32\" title=\"Matt Biilmann\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Matt Biilmann\" /></a><a href=\"https://twitter.com/suncat24\"><img width=\"32\" title=\"Anthony L\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Anthony L\" /></a><a href=\"https://twitter.com/bvdputte\"><img width=\"32\" title=\"Bart Vandeputte\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Bart Vandeputte\" /></a><a href=\"https://twitter.com/kdzwinel\"><img width=\"32\" title=\"Konrad Dzwinel\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Konrad Dzwinel\" /></a><a href=\"https://twitter.com/chilltymeTV\"><img width=\"32\" title=\"ChillTyme\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"ChillTyme\" /></a><a href=\"https://twitter.com/maiis\"><img width=\"32\" title=\"Emmanuel Vuigner\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Emmanuel Vuigner\" /></a><a href=\"https://twitter.com/utrenkner\"><img width=\"32\" title=\"Uwe Trenkner\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Uwe Trenkner\" /></a><a href=\"https://twitter.com/techieV2\"><img width=\"32\" title=\"Sriram Velamur (SMV)\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Sriram Velamur (SMV)\" /></a><a href=\"https://twitter.com/jamesmkenny\"><img width=\"32\" title=\"James Kenny\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"James Kenny\" /></a><a href=\"https://twitter.com/victormustar\"><img width=\"32\" title=\"Victor M\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Victor M\" /></a><a href=\"https://twitter.com/lkemen\"><img width=\"32\" title=\"Lise Kemen\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Lise Kemen\" /></a><a href=\"https://twitter.com/GMouron\"><img width=\"32\" title=\"Guillaume Mouron\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Guillaume Mouron\" /></a><a href=\"https://twitter.com/pconnors\"><img width=\"32\" title=\"Patrick Connors\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Patrick Connors\" /></a><a href=\"https://twitter.com/champjss\"><img width=\"32\" title=\"Jatesadakarn\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Jatesadakarn\" /></a><a href=\"https://twitter.com/asdrubalivan\"><img width=\"32\" title=\"Asdrúbal Iván 🇻🇪\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Asdrúbal Iván 🇻🇪\" /></a><a href=\"https://twitter.com/themizarkshow\"><img width=\"32\" title=\"Markolas\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Markolas\" /></a><a href=\"https://twitter.com/chiteri\"><img width=\"32\" title=\"Martin Akolo Chiteri\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Martin Akolo Chiteri\" /></a><a href=\"https://twitter.com/teleclimber\"><img width=\"32\" title=\"Olivier Forget\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Olivier Forget\" /></a><a href=\"https://twitter.com/fredrikanderzon\"><img width=\"32\" title=\"Fredrik Andersson\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Fredrik Andersson\" /></a><a href=\"https://twitter.com/blessingefkt\"><img width=\"32\" title=\"Blessing Richardson\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Blessing Richardson\" /></a><a href=\"https://twitter.com/MrMartineau\"><img width=\"32\" title=\"Zander\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Zander\" /></a><a href=\"https://twitter.com/hashchange\"><img width=\"32\" title=\"_ michael\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"_ michael\" /></a><a href=\"https://twitter.com/Jolg42\"><img width=\"32\" title=\"✨ Joël\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"✨ Joël\" /></a><a href=\"https://twitter.com/pgrucza\"><img width=\"32\" title=\"Peter Grucza\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Peter Grucza\" /></a><a href=\"https://twitter.com/jasoncartwright\"><img width=\"32\" title=\"Jason Cartwright\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Jason Cartwright\" /></a><a href=\"https://twitter.com/babacarcissedia\"><img width=\"32\" title=\"Dicky Ndiaye Johnson\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Dicky Ndiaye Johnson\" /></a><a href=\"https://twitter.com/Flocke\"><img width=\"32\" title=\"Jens Grochtdreis\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Jens Grochtdreis\" /></a><a href=\"https://twitter.com/stuartfrisby\"><img width=\"32\" title=\"Stuart Clarke-Frisby\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Stuart Clarke-Frisby\" /></a><a href=\"https://twitter.com/haroenv\"><img width=\"32\" title=\"Haroen\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Haroen\" /></a><a href=\"https://twitter.com/davidpich\"><img width=\"32\" title=\"David Pich\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"David Pich\" /></a><a href=\"https://twitter.com/simonw\"><img width=\"32\" title=\"Simon Willison\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Simon Willison\" /></a><a href=\"https://twitter.com/VladStirbu\"><img width=\"32\" title=\"Vlad Știrbu\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Vlad Știrbu\" /></a><a href=\"https://twitter.com/NvrRtrnToZork\"><img width=\"32\" title=\"let name =\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"let name =\" /></a><a href=\"https://twitter.com/benjaminlistwon\"><img width=\"32\" title=\"Benjamin Listwon\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Benjamin Listwon\" /></a><a href=\"https://twitter.com/LeBenLeBen\"><img width=\"32\" title=\"Benoît Burgener\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Benoît Burgener\" /></a><a href=\"https://twitter.com/NickersFpdx\"><img width=\"32\" title=\"Nicholas Fazzolari\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Nicholas Fazzolari\" /></a><a href=\"https://twitter.com/ArticleSeven\"><img width=\"32\" title=\"Article Seven\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Article Seven\" /></a><a href=\"https://twitter.com/ndrwknx\"><img width=\"32\" title=\"Andrew Knox\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Andrew Knox\" /></a><a href=\"https://twitter.com/gamesover\"><img width=\"32\" title=\"James Moberg\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"James Moberg\" /></a><a href=\"https://twitter.com/LoicBourg63\"><img width=\"32\" title=\"Loïc BOURG\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Loïc BOURG\" /></a><a href=\"https://twitter.com/wellis321\"><img width=\"32\" title=\"(((NoIndexNoFollow)))\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"(((NoIndexNoFollow)))\" /></a><a href=\"https://twitter.com/fresnault\"><img width=\"32\" title=\"François\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"François\" /></a><a href=\"https://twitter.com/sebmolines\"><img width=\"32\" title=\"Seb 🌱\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Seb 🌱\" /></a><a href=\"https://twitter.com/YannPdeM\"><img width=\"32\" title=\"YannPicarddeMuller\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"YannPicarddeMuller\" /></a><a href=\"https://twitter.com/shadeed9\"><img width=\"32\" title=\"Ahmad Shadeed\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Ahmad Shadeed\" /></a><a href=\"https://twitter.com/bennettfeely\"><img width=\"32\" title=\"Bennett Feely\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Bennett Feely\" /></a><a href=\"https://twitter.com/ryentzer\"><img width=\"32\" title=\"Rick Yentzer\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Rick Yentzer\" /></a><a href=\"https://twitter.com/adamrecvlohe\"><img width=\"32\" title=\"Adam-A zATE\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Adam-A zATE\" /></a><a href=\"https://twitter.com/ak_lap\"><img width=\"32\" title=\"Alexis La Porte\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Alexis La Porte\" /></a><a href=\"https://twitter.com/CosminOnciu\"><img width=\"32\" title=\"florin cosmin onciu\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"florin cosmin onciu\" /></a><a href=\"https://twitter.com/simongeorges\"><img width=\"32\" title=\"Simon Georges\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Simon Georges\" /></a><a href=\"https://twitter.com/ColmDelaney1\"><img width=\"32\" title=\"Colm Delaney\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Colm Delaney\" /></a><a href=\"https://twitter.com/ohhelloana\"><img width=\"32\" title=\"Ana Rodrigues\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Ana Rodrigues\" /></a><a href=\"https://twitter.com/rickbutterfield\"><img width=\"32\" title=\"Rick Butterfield\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Rick Butterfield\" /></a><a href=\"https://twitter.com/jewelbarnettphd\"><img width=\"32\" title=\"Jewel Barnett, Ph.D.\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Jewel Barnett, Ph.D.\" /></a><a href=\"https://twitter.com/nhoizey\"><img width=\"32\" title=\"Nicolas Hoizey\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Nicolas Hoizey\" /></a><a href=\"https://twitter.com/EdgarBarrantes\"><img width=\"32\" title=\"Edgar Barrantes\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Edgar Barrantes\" /></a><a href=\"https://twitter.com/xwoody\"><img width=\"32\" title=\"Aleks Hudochenkov\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Aleks Hudochenkov\" /></a><a href=\"https://twitter.com/jlbellorint\"><img width=\"32\" title=\"José Bellorín\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"José Bellorín\" /></a><a href=\"https://twitter.com/vurso\"><img width=\"32\" title=\"Tahir Khalid\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Tahir Khalid\" /></a><a href=\"https://twitter.com/bayes\"><img width=\"32\" title=\"Arek\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Arek\" /></a><a href=\"https://twitter.com/claymill\"><img width=\"32\" title=\"Clayton Miller\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Clayton Miller\" /></a><a href=\"https://twitter.com/rendall\"><img width=\"32\" title=\"Rendall\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Rendall\" /></a><a href=\"https://twitter.com/yavorski\"><img width=\"32\" title=\"Yavorski\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Yavorski\" /></a><a href=\"https://twitter.com/ripter001\"><img width=\"32\" title=\"ripter001\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"ripter001\" /></a><a href=\"https://twitter.com/WKamovitch\"><img width=\"32\" title=\"𝕨𝕚𝕝𝕝 𝕜𝕒𝕞𝕠𝕧𝕚𝕥𝕔𝕙\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"𝕨𝕚𝕝𝕝 𝕜𝕒𝕞𝕠𝕧𝕚𝕥𝕔𝕙\" /></a><a href=\"https://twitter.com/zachleat\"><img width=\"32\" title=\"Zach Leatherman\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Zach Leatherman\" /></a><a href=\"https://twitter.com/davatron5000\"><img width=\"32\" title=\"Dave Rupert\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Dave Rupert\" /></a><a href=\"https://twitter.com/relequestual\"><img width=\"32\" title=\"Ben Hutton\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Ben Hutton\" /></a><a href=\"https://twitter.com/kojote\"><img width=\"32\" title=\"Nils 'Das ist aber euer letztes Kind, oder?' Hitze\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Nils 'Das ist aber euer letztes Kind, oder?' Hitze\" /></a><a href=\"https://twitter.com/iChris\"><img width=\"32\" title=\"Chris Enns 👨🏻💻\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"Chris Enns 👨🏻💻\" /></a><a href=\"https://twitter.com/davchana\"><img width=\"32\" title=\"𝔻𝕒𝕧 ℂ𝕙𝕒𝕟𝕒\" height=\"32\" src=\"https://remysharp.com/images/no-user.svg\" alt=\"𝔻𝕒𝕧 ℂ𝕙𝕒𝕟𝕒\" /></a>\n\n \n \n <a href=\"https://remysharp.com/work\">Was this useful? You can hire me!</a>", | |
"text": "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.\nI was rather pissed that my blog contributes to Facebook's data so it was time to eject Disqus and look at the alternatives.\n\nDisqus is free…but not free at all\nWhen 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.\nAs Daniel Schildt / @autiomaa pointed out on twitter\n\nDisqus is even worse when you realise that their main product data.disqus.com is their own tracking dataset.\n\"The Web's Largest First-Party Data Set\"\n\nNot 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?\nOptions\nI'd already been window shopping for an alternative commenting system. There's a few github 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.\nThere'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.\n\nMouthful\nSchnack\nIsso\n\nAll of these options would be ad-free, tracking free, typically support exported Disqus comments and be open source. I personally settled on Commento for a few reasons:\n\n\"Cloud\" (ie. commercial) and self-hosted option\n\nOpen source and accepting issues and merge requests\nUser interface was clean and polished\n\nI'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 kicked me into the 100K views in a month and shot the post into 1st place on my popular page.\nThat said, if I do run into limits, I can move to a self hosted version with Scaleway or DigitalOcean or the like for €3-$5 a month.\nThe 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.\nCommento TL;DR\nThese 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.\n\nTesting offline isn't possible because the server reads the browser's location\nIf disqus' comment URLs don't match the post URLs (for any reason) the comments won't appear\nOrdered by oldest to newest\nAvatars are lost in export to import\nPerformance optimisations could be done (local CSS, accessibly colour contrast, etc)\n\nIf you want to use my commento.js and commento.css you're welcome to (and these are the contrast changes).\nThat all said and done, the comments are live, and look great now that I'm not feeding the Facebook beast.\n\nTesting Commento offline & adjusting urls\nThe commento.js script will read the location object for the host and the path to the comments. This presents two problems:\n\nTesting offline isn't possible as the host is localhost\n\nWhen I migrated from one platform to another (back from wordpress to my own code), my URLs dropped the slash - this means that no comments are found for the page\n\nThe 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).\nSo we'll override the parent:\nwindow.parent = {\n location: {\n host: 'remysharp.com',\n pathname: '/2019/04/04/how-i-failed-the-a/'\n }\n};\n\nNow 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 /).\nCaveat: messing with the parent value at the global scope could mess with other libraries. Hopefully that don't rely on these globals anyway.\nAlternative self hosted solution\nAnother methods is to download the open source version of commento.js front end library and make a few changes - which is what I've needed to do in the end.\nFirstly, 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:\nnoFonts = attrGet(scripts[i], 'data-no-fonts');\nDOMAIN = attrGet(scripts[i], 'data-domain') || parent.location.host;\nPATH = attrGet(scripts[i], 'data-path') || parent.location.pathname;\n\nI'm using the commento.js script with data attributes, which in the long run, is probably safer than messing with parent:\n<script defer async\n data-path=\"/2019/04/04/how-i-failed-the-a/\"\n data-domain=\"remysharp.com\"\n src=\"/js/commento.js\"></script>\n\nIt's important that the self-hosted version lives in /js/commento.js as it's hard coded in the commento.js file.\nOrdered by most recent comment\nBy default, Commento shows the first comment first, so the newest comments are at the end. I prefer most recent at the top.\nWith the help of flex box and some nice selectors, I can reverse all the comments and their sub-comments using:\n#commento-comments-area > div,\ndiv[id^=\"commento-comment-children-\"] {\n display: flex;\n flex-direction: column-reverse;\n}\n\nPreserving avatars\nDuring 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.\nThis 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 and save the filename under the real name.\nI 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.\nThis can all be done in single line of execution joining up unix tools:\n$ grep '<username>' remysharp-2019-06-10T16:15:12.407186-all.xml -B 2 |\n egrep -v '^--$' |\n paste -d' ' - - - |\n sort |\n uniq |\n grep -v \"'\" |\n awk -F'[<>]' '{ print \"wget -q https://disqus.com/api/users/avatars/\" $11 \".jpg -O \\\\\\\"\" $3 \".jpg\\\\\\\" &\" }' |\n xargs -I CMD sh -c CMD\n\nYou 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 to load these avatars.\nHow the combined commands work\nIn little steps:\ngrep '<username>' {file} -B 2\nFind the instance of <username> but include the 2 previous lines (which will catch the user's name too).\negrep -v '^--$'\nWhen 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 -\".\npaste -d' ' - - -\nThis will join lines (determined by the number of -s I use) and join them using the delimiter ' ' (space).\nsort | uniq\nWhen getting unique lines, you have to sort first.\ngrep -v \"'\"\nI'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.\nawk -F'[<>]' …\nThis 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.\nxargs -I CMD sh -c CMD\nThis means \"take the line from input and execute it wholesale\" - which triggers (in my case, 807) wget requests as background threads.\nIf you want learn more about the command line, you can check out my online course (which has a reader's discount applied 😉).\nThe 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 https://download.remysharp.com/comments/avatars/rem.jpg\nJavaScript to load these avatars\nInside 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.\nI've gone ahead and changed that logic so that it reads:\nIf 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.\nIf and only if, the image fires the error event, I then create the originally Commento element and replace the failed image with the div.\nThen, 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.\navatar = create('img');\navatar.setAttribute(\n 'data-src',\n `https://download.remysharp.com/comments/avatars/${\n commenter.name\n }.jpg`\n);\nclassAdd(avatar, 'avatar-img');\navatar.src = '/images/no-user.svg';\navatar.onerror = () => {\n var div = create('div');\n div.style['background'] = color;\n div.innerHTML = commenter.name[0].toUpperCase();\n classAdd(div, 'avatar');\n avatar.parentNode.replaceChild(div, avatar);\n};\n\nAs 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.\nTo do this I need to configure Commento to let me do a manual boot using the data-auto-init=\"false\" attribute on the script tag.\nOnce 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:\nfunction loadCommento() {\n if (window.commento && window.commento.main) {\n window.commento.main(() => observerImages());\n } else {\n setTimeout(loadCommento, 10);\n }\n}\nsetTimeout(loadCommento, 10);\n\nNote 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\nAccessibility and performance\nThe 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).\nIt didn't take too much though (I'm going to assume you're okay reading the nested syntax - I use Less, you might use SCSS, if not, remember to unroll the nesting):\nbody .commento-root {\n .commento-logged-container .commento-logout,\n .commento-card .commento-timeago,\n .commento-card .commento-score,\n .commento-markdown-button {\n color: #757575;\n }\n\n .commento-card .commento-option-button,\n .commento-card .commento-option-sticky,\n .commento-card .commento-option-unsticky {\n background: rgb(73, 80, 87);\n }\n}\n\nI 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 - both of which are worthwhile as pull requests to the project.\n\nSo 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…\n\n Posted 11-Jun 2019 under web & code.\n \n 👍 78 likes\n \n\n\n \n \n Was this useful? You can hire me!" | |
}, | |
"mention-of": "https://www.zachleat.com/web/facepile/", | |
"wm-property": "mention-of", | |
"wm-private": false | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment