Ant Smith

Articles

Why I adopted Free Love and evicted Facebook from my website

In this article you will read about

  • Facebook integration pitfalls

  • How to implement a low-friction like system

  • How to provide dynamic page updates (ie. update without reload) with AJAX

I have integrated Facebook Like and Comments plug-ins on my website since about version 2 of their SDK - they're now up to version 9. It has been a constant chore, with some change or other becoming required every 6 months or so. Not to mention the overhead of managing things like their 'Data Use Checkups'.

But even with the constant attention Facebook integration demands, the results have just never been ideal. The latest state of play is that nothing works very well at all if the visitor to my website is not already logged-in to Facebook. I think this is because my website serves over HTTP not HTTPS; and when I move to HTTPS everything will be dandy, maybe. Probably not.

You'll get an idea of how frustrating the Facebook integration has been by reading my article on how to do it.

I've also noted recently, the performance impact on my website that Facebook integration causes. Around about 1/3rd of all resources (bandwidth, execution time, load time etc...) gets consumed by Facebook. All of my pages load significantly, notably, faster without the like and comments plug-ins.

AND, obviously, I hate Facebook and I hate that their synical attitude to data-mining effects my website visitors. I just cannot in all good conscience continue to validate their existance.

So I have evicted Facebook from my website.

This means that at the moment there is no facility to leave a comment. I'm thinking about that - but the truth is people very rarely do leave comments, so I'm living without them for now (this may change).

The 'like' feature though was reasonably well used - it's an easy action for visitors and it is rewarding for me to see 'likes' appear. So I have written an alternative feature in to my website called 'FreeLove'. It doesn't require log-in and it doesn't give evil corporations access to my website visitor's browsing behaviours.

With FreeLove you can like any page on the website, with canonical awareness. That means if you happen to like one article from all of the articles on my site (eg. articles/freelove, this one), you will see that you have liked that article if you happen to be looking at only my technical articles (eg. articles/freelove/tag/technology). freelove and freelove/tag/technology are the same article presented in two different contexts - Facebook never properly understood this, and liking the one never showed up as liking the other. Idiots.

FreeLove also allows the liking of individual images, not just the page that they appear on. Facebook's like plug-in only allows liking of the URL (page) that the plug-in appears on; You couldn't like individual images. Image likes are also canonical. It doesn't matter if the image appears in a promotion panel (like on the home page), a gallery of related images, or on a page of its own - like it in one place and you will see that you like it wherever you happen to see it.

There is no log-in associated with FreeLove, so no concerns over registering an account, logging-in nor sharing of data collected under that log-in. You can like something and the only thing that happens is that my webserver increments a count of how many people have liked that something. No data whatsoever is mined.

Liking something does set a flag in your browser's local storage space to remember that you have liked the thing. But this is totally local data, not available to my server (or anyone else's).

The downside to not having to create an account, or log-in, or having your data shared with webservers, is that other browsers, or other devices, you use will not know what you have and haven't liked. That's just how it is. Liking is a very sweet and cool function - but it just isn't important enough to be worth all of the agro associated with demanding to know who exactly is liking this thing.

FreeLove Technology

There are four basic components to the FreeLove technology

  • Server Data - keeping a count of how many people have liked a thing

  • Server Interface - changing or retrieving the count data

  • User Data - Remembering (in a private way) wether a given user has liked a thing or not

  • User Interface - Displaying the count data and allowing a visitor to add their like (or later un-like)

FreeLove Server Data

There is a directory called 'likes' at the root of the webserver, which in turn contains one subdirectory for each section of the site.

When a thing is liked an associated text file is created (or updated) in the appropriate directory. This text file contains a count of the number of times that thing has been liked.

The name of the textfile is a crc32 hash of the thing's URI - which is not exactly human readable but URIs can be quite long and the maximum length of a linux filename is 255bytes.

It is important to note that the FreeLove like system indirectly allows visitors to the website to create files on the webserver. Thus it is theoretically possible for a hacker to flood the server with nonsense files. It is the duty of the Server Interface to prevent this.

FreeLove Server Interface

The endpoints for the like system looks like this:

<domain>/framework/likes.php?action=get&item=[URI]

<domain>/framework/likes.php?action=like&item=[URI]

<domain>/framework/likes.php?action=unlike&item=[URI]

<domain>/framework/likes.php?action=report&item=[URI]

<domain>/framework/likes.php?action=tally

<domain>/framework/likes.php?action=set&item=[URI]&value=[integer]

<domain>/framework/likes.php?action=clear&item=[URI]

Whatever action is taken, the URI is checked for validity; ie. that it references something that actually exists on the website. This ensures that arbitary nonsense files cannot be created through the interface.

The URI is converted to a filename using crc32. There is a possibility of collisions (2 different URIs generating the same crc32 value). For a website with upto 3,000 likeable assets the probability of collision is around 0.1%. This seems good enough. If a collision should occur the effect will be to aggregate the likes of two distinct items. Which is hardly the end of the world, but if it becomes a problem the hash algorithm will be changed and that will cause a headache in preserving like counts...

The FreeLove system only uses GET, LIKE and UNLIKE actions. The remaining actions are included to help with data maintenance (eg when moving from a prior like system).

These endpoints return JSON formatted data, providing:

  • hash: the crc32 hash value of the URI. This prevents the need for the client to calculate the canonical version of the URI.

  • count: the number of times the canonical version of the supplied URI has been liked, after any action (eg. like, unlike) has been performed

  • source: the requested URI

  • category: the site section referenced by the URI

  • status: confirms the action taken.

Note that for Tally the count is the total of all likes across the site and the category is an array of counts for each section of the site. Eg.:

http://antsmith.net/framework/likes.php?action=tally

{"hash":"164b9b11","count":769,"source":"all","status":"tallied","category":{"home":"98","articles":438,"audio":0,"memes":5,"photography":162,"poetry":52,"policies":0,"products":2,"projects":3,"stories":0,"video":9}}

Note also that the clear action has the effect of setting the count of likes to 0 - it does not delete the file that conatins the count as I do not want delete actions performed by webserver scripts that are instantiated by user action.

FreeLove User Data

The FreeLove system uses the website visitor's browser's local storage. If the browser does not supply local storage (because it is an old browser), or if the local storage is full, or if security settings prohibit access to local storage then the visitor will not be able to like or unlike content. The number of likes (which is retrieved from the server) is still displayed.

When the visitor likes an item (a page or an image) the returned hash of the URI is saved to local storage so that the status for this visitor can be remembered.

When the visitor unlikes an item the hash value is removed from local storage in order to minimise the space consumed.

FreeLove User Interface

Each page displays a heart icon with a like count next to it. The icon is solid if the visitor has previously liked the item or else is shown in outline. Clicking on the icon toggles the like status of the page. Similarly for images in the pop-up lightbox.

On page load document ready (and on image displayed in lightbox) an AJAX request is issued to the GET endpoint. On successful completion of that request the retrieved like count is inserted into the heart-icon's innerHTML.

A similar AJAX conversation is initiated when the heart-icon is clicked. This action also updates the local storage and the visual indication of the heart icon.

Here's the Javascript that implements like/unlike for a page:

function toggleLike () {

if (storageAvailable("localStorage")) {

h = '';

d = window.location.pathname;

if (d.charAt(0) == "/") d = d.substr(1);

if (d==="") d = '/';

d = d.split("/tag/")[0];

d = d.split("/collection/")[0];

var el = document.getElementById("pageHeart");

if (el.classList.contains('far')) {

a='like';

}

else {

a='unlike';

}

$.ajax({

type: "GET",

url: "/framework/likes.php",

data: {item: d, action: a},

success: function(data){

var el = document.getElementById("pageHeart");

cnt = parseInt(JSON.parse(data).count); if (isNaN(cnt)) cnt = 0;

h = JSON.parse(data).hash

if (a==='like') {

localStorage.setItem(h,"1");

el.classList.remove("far");

el.classList.add("fas");

}

else {

localStorage.removeItem(h);

el.classList.remove("fas");

el.classList.add("far");

}

if (cnt==0) {

el.innerHTML = '';

}

else {

el.innerHTML = ' '+String(cnt);

}

}

});

}

else {

alert("Liking disabled by browser security");

}

}

And here's the javascript that initialises the FreeLove system for a page:

$( document ).ready(function() {

var el = document.getElementById("pageHeart");

if (storageAvailable("localStorage")) {

h = '';

cnt = 0;

d = window.location.pathname;

d = d.split("/tag/")[0];

d = d.split("/collection/")[0];

if (d.charAt(0) == "/") d = d.substr(1);

if (d==="") d = '/';

$.ajax({

type: "GET",

url: "/framework/likes.php",

data: {item: d, action: 'get'},

success: function(data){

cnt = parseInt(JSON.parse(data).count); if (isNaN(cnt)) cnt = 0;

h = JSON.parse(data).hash;

if (cnt==0) {

el.innerHTML = '';

}

else {

el.innerHTML = ' '+String(cnt);

}

if (h!=='') {

likedItem = false;

if (localStorage.getItem(h)=="1") {

likedItem = true;

}

if (likedItem) {

el.classList.remove("far");

el.classList.add("fas");

}

else {

el.classList.remove("fas");

el.classList.add("far");

}

}

}

});

}

el.addEventListener('click', function () { toggleLike();},false);

});

And finally, the associated HTML (note, we are using font-awesome icons):

<i id="pageHeart" class="far fa-heart"></i>

Articles