Tech, Cloud and Programming

Votes and Views Part 1

|

How to have views and votes on a static created website? In a server side website (php/react/etc) you would pull the counters from the database and inject in the code while serving. This website is static generated by Zola, every time i publish an article. Also cloudflare doesn't gives you access logs in the free version.

I could use Google Analytics to track people, but that would includes cookies and send a lot of data to Google. As a real engineer i wrote my own small version. This first article is about the client side, next part will be about the AWS backend.

Design

Up&Down vote, like/dislike are common among websites with content. The old style Youtube had 2 counters, one for like and another for dislike. Reddit uses a single number. Since the traffic on my blog is very low i keep it simple and just have some 1 counter.

Other criteria:

  • Intuitive interface
  • Responsive changes
  • Prevent multiple votes
  • No Cookies

My first iteration looks like this in the list: vote and like this on the article page top: vote I use the icons from fontawesome.com. With just this: <i class="fa-solid fa-circle-up"></i> you have a nice icon for an up vote. The eye shows the views and plus sign to read more.

Implementation

In the template code this shows as the following:

<a id="downvote" title="Downvote"  href="#" onclick="vote('down','{{ post.permalink }}');return false;"><i class="fa fa-arrow-circle-down"></i></a>
<span class="post-votecounter" id="VOTE#{{ post.permalink }}">0</span> 
<a id="upvote" title="Upvote"  href="#" onclick="vote('up','{{ post.permalink }}');return false;"><i class="fa fa-arrow-circle-up"></i></a>&nbsp;

<i class="post-viewcount fa-solid fa-eye"></i><span class="post-viewcount" id="VIEW#{{ post.permalink }}">1</span>

A simple onclick for that points to a javascript function. vote('down','{{ post.permalink }}') The view and vote counters are wrapped in a span with a specific id. id="VOTE#{{ post.permalink }}" later this is used to update the counter dynamically.

The vote script does a GET request to the RestAPI on AWS.

function vote(action,page = undefined) {
  var referer = page ||  new URL(window.location.href).href

  // check for continuos upvoting
  const storedVote = localStorage.getItem("VOTE#"+page);
  if(storedVote && storedVote == action) { return false; }

  var xhr = new XMLHttpRequest();
  xhr.open('GET', API_URL+"vote/"+action);
  xhr.responseType = 'json';
  xhr.setRequestHeader('X-Referer', referer);

  xhr.onreadystatechange = function () {
     if (xhr.readyState === 4) {
        myStorage.setItem("VOTE#"+page,action)
        document.getElementById("VOTE#"+page).innerHTML= xhr.response.Count
     }};
  
  //var data = event_str;
  xhr.send();
}

First to prevent multiple votes i use LocalStorage. It stores the page and the action, if you try to upvote or downvote twice it refuses. The item is VOTE#https://jacob.verhoeks.org/votes-and-views-1/.

I expected the referer in the javascript to reflect the page the requests origins, but the browser uses the root url. To work around, i use a custom header: X-Referer to pass it on the backend. No Post data or GET url parameter required. The restapi /vote/up will do a ADD 1 in the counter for that page and returns the new value. /vote/down does add ADD -1.

The document.getElementById("VOTE#"+page).innerHTML= xhr.response.Count locates the right span that counts the vote count and updates the DOM dynamically.

Views

Views don't have an interactive option, they are registered whenever you visit a page. For this there is a javascript function that loads on every view. It sends a view update to the system. Besides the page i store the windows and viewport size to see how people watch my blog. Also the language is interesting to see where people come from.

function logView() {
  var page = new URL(window.location.href).href
  var xhr = new XMLHttpRequest();
  xhr.open('GET', API_URL+"log");
  xhr.responseType = 'json';
  xhr.setRequestHeader('X-Referer', page);
  xhr.setRequestHeader('X-Dimension', 'window='+window.screen.width+'x'+window.screen.height+",viewport="+window.innerWidth+"x"+window.innerHeight);
  xhr.setRequestHeader('X-Language', window.navigator.language);

  xhr.onreadystatechange = function () {
    if (xhr.readyState === 4) {
       element =document.getElementById("VIEW#"+page);
       if(element) {       element.innerHTML= xhr.response.Count }
    }};
 
    xhr.send();
}

Updating mode

To update the page while normally browsing there is a function running and every page view, it retries all votes and views for the website and update the DOM if it exists on the page.

function loadItems() {

  var xhr = new XMLHttpRequest();
  xhr.open('GET', API_URL+"votes");
  xhr.responseType = 'json';

  xhr.onreadystatechange = function () {
     if (xhr.readyState === 4) {
       if(xhr.status == 200) {
          Object.keys(xhr.response.votes).forEach(function (key) {
            let vote = xhr.response.votes[key];
            let page = vote.Url;
            element =  document.getElementById(page);
            if(element) {
              element.innerHTML= vote.Count
            }
          });
       }
     }};
    xhr.send();
}

Backend

Next blog will be about the backend. I uses CDK / Typescript / API Gateway / DynamoDB , but no lambda's.