Votes and Views Part 1
2022-02-25 | aws javascript restHow 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: and like this on the article page top:
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>
<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.