Visitors are also reading...
← PreviousNext →

Add pageviews and news feed to static website with firebase

15 Aug 2016

Introduction

Previously I wrote about how to setup a static website with metalsmith.
Today, I will describe how you can add dynamic and persistent content to your static website using firebase

Why firebase?

There are plenty of ways to add persistency and dynamicity to static websites today.
Heroku, for example, is one of them.
In fact, every online hosting combines with DB as a service could fit.
Firesbase is just one of those services. So why choose it?

Which makes it perfect for the task at hand.

Initialize the client

My goal is to get the blog post readable as soon as possible, and so I want to load the firebase client asynchronously. So I used jquery's getScript method.

And afterwards I initialized the firebase client by copy-pasting from their website. The process is painless.


$.getScript('https://www.gstatic.com/firebasejs/3.2.1/firebase.js', function () {

/**
 * initialize
 * @type \{{apiKey: string, authDomain: string, databaseURL: string, storageBucket: string}\}
 */
// Initialize Firebase
var config = {
  apiKey: "AIzaSyBPkaNJa7n7yb670VB07t-6SO5nGSJ_3R0",
  authDomain: "mograblog.firebaseapp.com",
  databaseURL: "https://mograblog.firebaseio.com",
  storageBucket: ""
};
firebase.initializeApp(config);
var rootRef = firebase.database().ref();

The apiKey looks like secret information, but in my case it is not.
If you keep communication with firebase in the backend, then you shouldn't worry.
But in my case communication is from frontend, so I need to be careful.
For this purpose, firebase invented a rules mechanism.

The features I want and their Data-Structure

I wanted 2 features

The data structure I decided on is

 + root
     + pageviews:Map<String,Integer> - a map from page url to views 
     + currentlyReading:Array<Item>
        + Item 
            - timestamp:long
            - title:String
            - url:String

And now, I have to make writing to firebase safe by validating the information

The Rules

The source below describes the following rules:

Hopefully these validation are enough to make me sleep good at night.
Here is how it looks like in the firebase console

{
  "rules": {
    ".read" : "true",
    ".write" : "true",
    "pageviews" : {
      "$page" : {
        ".validate" :  "newData.isNumber() && ( !data.exists() || newData.val() == data.val() + 1)"
      }
    },
    "currentlyReading" : {
      ".validate" : "!newData.hasChild('11')",
      "$index" : {
        ".validate" : "newData.child('title').isString() && newData.child('url').isString()"
      }
    }
  }
}

Lets walk through the rules

And now to the client code

Update pageviews

var pageViewsRef = rootRef.child('pageviews');
if (window && window.location && window.location.pathname) {

  /****
   *
   * update page views
   *
   ****/
  var pathkey = slug(window.location.pathname);
  pageRef = pageViewsRef.child(pathkey);
  var counted = false;

  pageRef.on('value', function (pageviews) {
    var pageviewsCount = pageviews.val();
    var $pageviews = $('.pageviews');
    $pageviews.attr('title', pageviewsCount + ' people saw this post');
    $pageviews.text(pageviewsCount);
    $pageviews.addClass('initialized');
    if (!counted) {
      counted = true;
      pageRef.transaction(function (views) {
        return views + 1;
      })
    }
  });
}

First I slugify pathname. (slugging makes the pathname legal json key and keeps it readable).
Then I register to value event. On each value change I update the DOM.
If this is the first event, I also update the value.

You might be wondering why I first register to the value event, update the dom and only then update the value in firebase.
This causes the value event to fire twice on each page load.

The reason I did it is that the event is fired twice anyway - no matter the order in which I write the code.
However, if I first update the value, the first value event will show 1, and with a delay it will retrieve the updated value.

At least this way I don't show anything until we have the most updated value to that point.

How cool is it that this code will update the counter LIVE!

Update the 'currently reading' section

The code below takes the same approach of 'first read the value then update it' so to display a very updated value first.

 /******
  *
  * update currently reading
  *
  *******/
var currentReadingRef = rootRef.child('currentlyReading');
var currentlyReading = false;
currentReadingRef.on('value', function (currentlyReadingValue) {

  /*****
   *
   * construct the twitter-feed like display
   *
   *****/
  var $currentlyReading = $('.currently-reading');
  $currentlyReading.empty();
  currentlyReadingValue.val().forEach(function (value) {
    $currentlyReading.append(
      $('<a>', {text: value.title, href: value.url, 'class': 'currently-reading-link mograblog-link'})
    );

    var agoText = 'earlier..';
    if (value.timestamp) {
      var hoursAgo = Math.floor(( Date.now() - value.timestamp ) / ( 1000 * 60 * 60 ));
      agoText = hoursAgo < 2 ? 'right now..' : hoursAgo + ' hours ago';
    }

    $currentlyReading.append(
      $('<div>', {text: agoText, 'class': 'currently-reading-ago'})
    )
  });
  $('.currently-reading-wrapper').addClass('initialized');

  /**
   *
   * update the value
   *
   */
  if (!currentlyReading) { // modify only once
    currentlyReading = true;
    currentReadingRef.transaction(function (currentlyReadingArray) {
      var postTitle = $('.post-title').text();
      try {
        if (currentlyReadingArray && currentlyReadingArray.length > 0 && currentlyReadingArray[0].title == postTitle) { // don't modify if already registered this post
          return;
        }
      } catch (e) {
      }
      return [{ // add this post and append with all the rest
        title: postTitle,
        url: document.location.pathname,
        timestamp: Date.now()
      }].concat(currentlyReadingArray.slice(0, currentlyReadingArray.length - 1))
    })
  }
})

← PreviousNext →