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.