WordPress
Article

Introducing the SitePoint Random Hello Bar WordPress Plugin

By Brad Denver

If you’re a regular SitePoint reader, you may have noticed a small feature we refer to as the Random Hello Bar. If you scroll far enough down this page you should see it slide in from the top of the screen. It should look something like this:

An image showing an example of what the SitePoint Random Hello Bar looks like when deployed

We feel it’s an unobtrusive way of adding advertising, product announcements or other messages to a page, so we thought it was time to share it with you. In this article I’m going take you through how we put it together and then show some examples of how you can truly make it your own. If you prefer to just skip to the code, it’s available on GitHub, npm or the WordPress Plugin Directory.

WordPress Admin Interface

The entry point to our plugin is sp-random-hello-bar.php and the plugin’s main class at src/SitePoint/RandomHelloBar.php.


//sp-random-hello-bar.php

require_once(plugin_dir_path( __FILE__ ).'src/SitePoint/RandomHelloBar.php');

\SitePoint\RandomHelloBar::public_actions();

if (is_admin()) {
  \Sitepoint\RandomHelloBar::admin_actions();
}

//src/SitePoint/RandomHelloBar.php

namespace SitePoint;

class RandomHelloBar {
  const PLUGIN_NAME = 'sp-random-hello-bar';

  private static function get_option($option) {
    return get_option(self::PLUGIN_NAME.'-'.$option);
  }

  private static function update_option($option, $value) {
    return update_option(self::PLUGIN_NAME.'-'.$option, $value);
  }

  private static function delete_option($option) {
    return delete_option(self::PLUGIN_NAME.'-'.$option);
  }

}

To create our admin UI we’re going take advantage of the WordPress Settings API as it allows admin pages containing settings forms to be managed semi-automatically. We add a SP Random Hello Bar sub-menu under the Settings menu via the add_options_page function. Then we register sp-random-hello-bar-enabled and sp-random-hello-bar-ads settings and the sections they belong to.


public static function admin_actions() {
  add_action('admin_init', '\Sitepoint\RandomHelloBar::admin_init');

  add_action('admin_menu', function() {
    add_options_page('SP Random Hello Bar', 'SP Random Hello Bar', 'manage_options', self::PLUGIN_NAME, '\SitePoint\RandomHelloBar::options_page');
  });
}

public static function admin_init() {
  register_setting(self::PLUGIN_NAME.'-settings-group', self::PLUGIN_NAME.'-enabled');
  register_setting(self::PLUGIN_NAME.'-settings-group', self::PLUGIN_NAME.'-ads', '\SitePoint\RandomHelloBar::sanitize_ads');

  add_settings_section(self::PLUGIN_NAME.'-section-one', 'Settings', '\SitePoint\RandomHelloBar::section_one_help', self::PLUGIN_NAME);

  add_settings_field(self::PLUGIN_NAME.'-enabled', 'Enabled', function() {
      $setting = esc_attr(self::get_option('enabled'));
      include dirname( __FILE__ ).'/../views/admin/enabled-fields.php';
  }, self::PLUGIN_NAME, self::PLUGIN_NAME.'-section-one');

  add_settings_section(self::PLUGIN_NAME.'-section-two', 'Hello Bar Ads', '\SitePoint\RandomHelloBar::section_two_help', self::PLUGIN_NAME);

  add_settings_section(self::PLUGIN_NAME.'-field-two', null, '\SitePoint\RandomHelloBar::ad_fields', self::PLUGIN_NAME);
}

public static function  options_page() {
  include dirname( __FILE__ ).'/../views/admin/options_page.php';
}

One point of interest is a little trick we have used to create a one-click delete button in src/views/admin/ad_fields.php that uses a label styled as a button and a hidden checkbox.


<label class="button">
  <input type="checkbox" name="< ?php echo self::PLUGIN_NAME; ?>-ads[< ?php echo $key; ?>][delete]" value="1" onchange="this.form.submit()" style="display: none;" />Delete
</label>

The sp-random-hello-bar-ads setting will hold an array of a values, each composed of an HTML and weight value. You may have noticed the last argument passed to register_setting() for sp-random-hello-bar-ads. This is a sanitize callback function that’s called before the setting is saved. In this case it serves two purposes. It removes any invalid elements, as well as any flagged for deletion (i.e. the button was clicked). Once sanitized, the values are passed to save_weighted_ads so we can save some extra settings: weighted-keys and total-weight, that we’ll need later when picking an item to show.


public static function sanitize_ads($input) {

  foreach($input as $key => $ad) {
    if(!$ad['html'] || !is_numeric($ad['weight']) || isset($ad['delete'])) {
      unset($input[$key]);
    }
  }

  self::save_weighted_ads($input);

  return $input;
}

public static function save_weighted_ads($ads) {
  $weighted_array = array();
  $total_weight   = 0;

  foreach($ads as $key => $val){
    $total_weight += $val['weight'];
    for($i=0; $i< $val['weight']; $i++) {
      $weighted_array[] = $key;
    }
  }

  self::update_option('weighted-keys', $weighted_array);
  self::update_option('total-weight', $total_weight);
}

We now have a functioning UI to enter Random Hello Bar content and enable/disable the feature.

Ajax Endpoint

It’s all well and good being able to enter Random Hello Bar content but at this point there’s no way to retrieve content on demand. Let’s fix that.

First, we register Ajax hooks for logged-in and logged-out users.


public  static function public_actions() {
  if (!\SitePoint\RandomHelloBar::get_option('enabled')) return;

  add_action('wp_ajax_nopriv_get_random_hello_bar', '\SitePoint\RandomHelloBar::get_random_bar');
  add_action('wp_ajax_get_random_hello_bar', '\SitePoint\RandomHelloBar::get_random_bar');
}

Both hooks call the following method.


public static function get_random_bar() {
  $weighted_keys = self::get_option('weighted-keys');
  $total_weight  = self::get_option('total-weight');
  $rand          = floor($_POST['rand'] * $total_weight);

  if(!$weighted_keys || !$total_weight || !$_POST['rand']) die();

  $ads = self::get_option('ads');

  if(!$ads) die();

  if(!isset($weighted_keys[$rand])) die();

  echo wp_kses_post($ads[$weighted_keys[$rand]]['html']);

  die();
}

How does it work? Say we’ve saved three bars as follows:

  1. html: bar1, weight: 2
  2. html: bar2, weight: 1
  3. html: bar3, weight: 1

We would expect bar1 to show 50% of the time and bar2 and bar3 to each show 25% of time. Let’s also assume the client has sent through a $_POST['rand'] value of 0.33. $_POST['rand'] should always be a value from 0 to 1, such as that generated by the JavaScript Math.random() function. We rely on the $_POST['rand'] value sent from the client rather than generating a random number on the server as this is less likely to be cached. Given these values, get_random_bar() would work as follows:


$weighted_keys = self::get_option('weighted-keys'); // [0, 0, 1, 2]
$total_weight  = self::get_option('total-weight'); // 4
//we can then turn $_POST['rand'] into a number between 0 and $total-weight -1 by doing
$rand          = floor($_POST['rand'] * $total_weight); // floor(0.33 * 4) = 1

$ads = self::get_option('ads'); // [0 => ['html' => 'bar1', 'weight => 2], 1 => ['html' => 'bar2', 'weight' => 1], 2 => ['html' => 'bar3', 'weight' => 1]]

if(!isset($weighted_keys[$rand])) die(); // $weighted_keys[$rand] = $weighted_keys[1] = 0

//we then grab the value at index 1 of $weighted-keys, in this case 0
//and that is the index we use to retrieve a hello bar content from the saved ads option, which would be bar1
echo wp_kses_post($ads[$weighted_keys[$rand]]['html']); // $ads[0]['html'] = 'bar1'

To give another example, if $_POST['rand'] = 0.66:


$rand = 2
$weighted_keys[2] = 1;
$ads[1] = 'bar2';

Displaying Content

The server side of the plugin is ready to give you Random Hello Bars now, so it’s time for you to request and display them.

The mechanics of hiding and showing the Random Hello Bar on page scroll are all encapsulated in the sp-hello-bar JavaScript module. It’s included in the plugin at src/js/SpHelloBar.js and is also available on npm. The module has absolutely zero dependencies (not even jQuery) and has been designed to be as flexible as possible. For that reason the module itself requires a throttle function to be passed into its constructor and leaves it up to you to fetch the Random Hello Bar content and insert it into the DOM. The module is only concerned with initiating, hiding and showing the Random Hello Bar.

As default WordPress installs have both jQuery and Underscore.js available, so it’s easy to to create a basic script that fetches a Random Hello Bar, inserts it into the DOM and then initiates sp-hello-bar to bring it to life. It’s so easy we’ve included just such a script for you in the plugin at public/js/basic.js. That file has been compiled by Babel from the ES6 source code in src/js/basic.js.


import SpHelloBar        from "./SpHelloBar";
import getRandomHelloBar from "./helpers/getRandomHelloBar";

import "../css/basic.css";

(function(window, $, _) {

  const sph = new SpHelloBar({
    throttle: _.throttle
  });

  getRandomHelloBar($, () => sph.init());

})(window, jQuery, _);

As you can see, this script constructs an instance of SpHelloBar passing in the throttle function from Underscore.js (the throttle function is used to throttle window resize and scroll events). It then calls the helper function getRandomHelloBar:


export default function ($, cb) {
  $.ajax({
    type : "POST",
    url  : ajax_object.ajax_url,
    data : {
      action : "get_random_hello_bar",
      rand   : Math.random()
    },
    success: function(data, textStatus, XMLHttpRequest) {
      $("body").prepend(data);
      cb();
    }
  });
}

The function uses the jQuery.ajax method to request the Random Hello Bar content from our plugin. On success it inserts the content into the body and then fires the call back which we have set as SpHelloBar.init().

As I mentioned the sp-hello-bar module has been designed to be flexible and easily extended. An example of extending it would be to take note of when a user manually closes the Random Hello Bar and then not display it to them next time. As that’s a nice user experience we’ve also included that for you in public/js/basicStorage.js.


import SpHelloBar                          from "./SpHelloBar";
import { checkStorage, disableViaStorage } from "./helpers/disableViaStorage";
import getRandomHelloBar                   from "./helpers/getRandomHelloBar";

(function(window, $, _) {

  const sph = new SpHelloBar({
    throttle: _.throttle
  });

  // extend SpHelloBar
  sph.after("beforeInit", function() {
    checkStorage.call(sph);
  });
  sph.after("onClose", function() {
    disableViaStorage.call(sph);
  });

  getRandomHelloBar($, () => sph.init());

})(window, jQuery, _);

As you can see, it’s similar to the basic.js script but this time we make use of the after() method to hook onto the beforeInit and onClose stages. The checkStorage and disableViaStorage functions shown below are simple wrappers around local storage. The various lifecycle stages are all documented on GitHub.


const CAN_LOCAL_STORAGE = !!(window && window.localStorage);
const EXPIRE_DAYS       = 14;
const STORAGE_KEY       = 'SpHelloBarDisabled';

function futureDaysInMs(days = EXPIRE_DAYS) {
  return Date.now() + (1000 * 60 * 60 * 24 * days);
}

export function checkStorage() {
  // hello bar may be disabled for the number of days set in EXPIRE_DAYS when user manually closed the bar
  const expiry = (CAN_LOCAL_STORAGE) ? window.localStorage.getItem(STORAGE_KEY) : null;

  if (expiry !== null) {
    const now = Date.now();
    if(expiry > now) {
      this.isEnabled = false;
    } else {
      window.localStorage.removeItem(STORAGE_KEY);
    }
  }
}

export function disableViaStorage() {
  if (CAN_LOCAL_STORAGE) window.localStorage.setItem(STORAGE_KEY, futureDaysInMs());
}

The plugin doesn’t assume that everyone will want to use one of our basic scripts or basic CSS but it’s nice to make that easily enabled via the Admin UI. This can be easily done by adding a couple more settings to our options page.


public static function admin_init() {
  register_setting(self::PLUGIN_NAME.'-settings-group', self::PLUGIN_NAME.'-enabled');
  register_setting(self::PLUGIN_NAME.'-settings-group', self::PLUGIN_NAME.'-basic-js');
  register_setting(self::PLUGIN_NAME.'-settings-group', self::PLUGIN_NAME.'-load-css');
  register_setting(self::PLUGIN_NAME.'-settings-group', self::PLUGIN_NAME.'-ads', '\SitePoint\RandomHelloBar::sanitize_ads');

  add_settings_section(self::PLUGIN_NAME.'-section-one', 'Settings', '\SitePoint\RandomHelloBar::section_one_help', self::PLUGIN_NAME);

  add_settings_field(self::PLUGIN_NAME.'-enabled', 'Enabled', function() {
      $setting = esc_attr(self::get_option('enabled'));
      include dirname( __FILE__ ).'/../views/admin/enabled-fields.php';
  }, self::PLUGIN_NAME, self::PLUGIN_NAME.'-section-one');

  add_settings_field(self::PLUGIN_NAME.'-basic-js', 'Enqueue Basic JS', function() {
      $setting = esc_attr(self::get_option('basic-js'));
      include dirname( __FILE__ ).'/../views/admin/basic-js-fields.php';
  }, self::PLUGIN_NAME, self::PLUGIN_NAME.'-section-one');

  add_settings_field(self::PLUGIN_NAME.'-load-css', 'Enqueue Basic CSS', function() {
      $setting = esc_attr(self::get_option('load-css'));
      include dirname( __FILE__ ).'/../views/admin/load-css-fields.php';
  }, self::PLUGIN_NAME, self::PLUGIN_NAME.'-section-one');

  add_settings_section(self::PLUGIN_NAME.'-section-two', 'Hello Bar Ads', '\SitePoint\RandomHelloBar::section_two_help', self::PLUGIN_NAME);

  add_settings_section(self::PLUGIN_NAME.'-field-two', null, '\SitePoint\RandomHelloBar::ad_fields', self::PLUGIN_NAME);
}

Then updating public_actions to enqueue them if appropriate.


public  static function public_actions() {
  if (!\SitePoint\RandomHelloBar::get_option('enabled')) return;

  add_action('wp_enqueue_scripts', '\SitePoint\RandomHelloBar::enqueuePublicAssets');

  add_action('wp_ajax_nopriv_get_random_hello_bar', '\SitePoint\RandomHelloBar::get_random_bar');
  add_action('wp_ajax_get_random_hello_bar', '\SitePoint\RandomHelloBar::get_random_bar');
}

public static function enqueuePublicAssets() {
  $basic_js = \SitePoint\RandomHelloBar::get_option('basic-js');
  if (in_array($basic_js, array('basic', 'basicStorge'))) {
    wp_enqueue_script(
      self::PLUGIN_NAME.'-basic-script',
      plugins_url('../../public/js/'.$basic_js.'.js' , __FILE__),
      array('jquery', 'underscore')
    );
    wp_localize_script(
      self::PLUGIN_NAME.'-basic-script',
      'ajax_object',
      array('ajax_url' => admin_url( 'admin-ajax.php' ))
    );
  }

  if(\SitePoint\RandomHelloBar::get_option('load-css')) {
    wp_enqueue_style(
      self::PLUGIN_NAME.'-basic-css',
      plugins_url( '../../public/css/basic.css' , __FILE__ )
    );
  }
}

Make It Your Own

While the basic scripts provided with the plugin are quite useable, the real value comes in customizing it to suit your needs. What follows are just a few suggestions of what’s possible.

Only Load if Indicator Present

One potential issue with the basic script provided is that it will load the Random Hello Bar on all pages and that may not be what you want. If you alter your WordPress theme, to add .enableSpHelloBar to the main element in your single.php post template, for example, you could use the following script and only have the Random Hello Bar on particular post pages.


import SpHelloBar        from "./SpHelloBar";
import getRandomHelloBar from "./helpers/getRandomHelloBar";

import "../css/basic.css";

(function(window, $, _) {
  
  if (!document.getElementsByClassName('enableSpHelloBar').length) return;

  const sph = new SpHelloBar({
    throttle: _.throttle
  });

  getRandomHelloBar($, () => sph.init());

})(window, jQuery, _);

Tracking

Another common use case is to track the use of components in the page. The following script makes use of the onToggle and onClick event hooks to track when the Random Hello Bar is first displayed in the page and when its link is clicked.


import SpHelloBar        from "./SpHelloBar";
import getRandomHelloBar from "./helpers/getRandomHelloBar";
import trackEvent        from './helpers/trackEvent';

import "../css/basic.css";

(function(window, $, _) {

  const sph = new SpHelloBar({
    throttle: _.throttle
  });

  // extend SpHelloBar
  sph.after('onClick', function() {
    trackEvent.call(sph, 'Click');
  });
  sph.after('onToggle', function() {
    trackEvent.call(sph, 'Impression');
  });

  getRandomHelloBar($, () => sph.init());

})(window, jQuery, _);

export default function trackEvent (label) {
  // only track impressions once
  if (!this.impressionTracked && this.isShown && label == 'Impression') {
    this.impressionTracked = true;
  } else if(this.isShown && label == 'Impression') {
    return;
  }

  // check for the Google Analytics global var
  if (typeof(ga) == 'undefined') return;

  ga('send', 'event', 'SP Hello Bar', label, {
    'nonInteraction': true
  });
}

Random Hello Bar Trigger

By default the Random Hello Bar displays once the page is scrolled down 300px. The following script shows how you can add a custom data attribute to an element elsewhere on the page and use the element to determine the point at which the Random Hello Bar displays. For instance, you may add the attribute to an element after the main post body.


import SpHelloBar        from "./SpHelloBar";
import getRandomHelloBar from "./helpers/getRandomHelloBar";

import "../css/basic.css";

(function(window, $, _) {

  let targetOffset = 300;
  if(document.querySelector('[data-hellobar="trigger"]')) targetOffset = document.querySelector('[data-hellobar="trigger"]').offsetTop;

  const sph = new SpHelloBar({
    targetOffset,
    throttle: _.throttle
  });

  getRandomHelloBar($, () => sph.init());

})(window, jQuery, _);

Fixed Position Header

The navigation menus (the header) on SitePoint are statically positioned so they scroll up and down with the page content. Not all sites are set up that way. For example the SitePoint Forums have a fixed position header. It remains fixed at the top of the page as the content scrolls up and down.

The fix is very simple. The Random Hello Bar works by positioning itself above the top of the screen (a negative top value) when hidden and then changing that top value to zero, which is the top off the screen when visible. To allow for a fixed header you’d simply adjust the value of top for the header’s height.


.SpHelloBar {
  ...

  &.u-show {
    top: 56px;
  }
}

Conclusion

The SitePoint Random Hello Bar is a handy plugin that is ready to go straight out of the box but can quickly and easily be customised to suit the unique needs of your site. We hope you enjoy it, and feel free to ask any questions in the comments!

  • Ds

    I appreciate the tutorial and nothing against what you have done but I have to be honest this was one of the first things I blocked on Sitepoint.

    It has to be one of the most annoying forms of animation/advertising you can implement:

    1) It doesn’t add a padding to the top of the page so content at the top is hidden if you reach the bottom.

    2) The animation is jumpy on devices without hardware acceleration.

    3) The bar itself looks out of place on a desktop with the Browser UI and it takes up too much space on tablets+phones (chrome/safari for example with loose the top bar when you scroll to make room for the page).

    Sticky navigation I get but this…not so much.

  • http://wpviz.com/ Hammad Afzal

    Hi Brad,
    WONDERFUL PLUGIN POST.
    THANKS A LOT

Recommended
Sponsors
Because We Like You
Free Ebooks!

Grab SitePoint's top 10 web dev and design ebooks, completely free!

Get the latest in WordPress, once a week, for free.