Friday, July 10, 2015

The Plugin Trap - Tale of Google Maps Integration

WordPress plugins provide powerful features to one's WordPress site and at the same time they can introduce bugs, incompatible design elements, etc.

In this project that I am working on, we needed to have a page with an interactive map (aka. Google Maps) with a list of locations below it.

My first stab at this was to look for WordPress plugs for this.  My first criteria was to look for the plugins with large number of active installs, with good ratings, and recently updated.  I narrowed it down to:
My general findings were that:
  • Some plugins are no longer supported.
  • Many plugins offered options to support styled maps; often paid upgrades.
  • All the plugins provided an opinionated user interface for marker lists (i.e., limited styling).
  • All the plugins had complex administrative screens.
I did spend some extra time exploring WP Google Maps as it was the most promising (with the addition of the Gold version).  The key problems that I encountered, however, were:
  • The plugin had a complex administrative screen; we need to hand over the administration of the locations (markers) to the client.
  • The plugin provided an opinionated presentation of the marker list that would be incompatible without our highly styled site (deal breaker).
In the end, I opted to take a completely different approach; leveraging my background in client-side JavaScript.

1. First, I created a custom post type: https://codex.wordpress.org/Post_Types

wp-content/mu-plugins/locations.php
 
<?php
add_action( 'init', 'create_post_type' );
function create_post_type() {
  register_post_type( 'locations',
    array(
      'labels' => array(
        'name' => __( 'Locations' ),
        'singular_name' => __( 'Location' )
      ),
      'public' => false,
      'has_archive' => false,
      'show_ui' => true,
    )
  );
}

By adding this custom post type, I was able to create a top level menu in the administrative screen to manage locations that were only available through PHP code, e.g., no routes are created for viewing an individual or archive of them.

2. Created Custom Fields for Locations Posts

Because we want to show a lot of information (in addition to latitude and longitude) for each location, used the Advanced Custom Fields plugin to create a field group (show for the custom locations post type), e.g.,
  • type - list
  • latitude - text
  • longitude - text
  • continent - list
  • country - list
  • address 1 - text
  • address 2 - text
  • address 3 - text
  • phone - text
3. Created a Page with Custom Template

Create a page to display the location and a custom template for it, e.g., copy page.php to  page-locations.php

The overall strategy that I employed here is to quickly get the data out of WordPress (and PHP) and then use client-side JavaScript to build the user interface from the data.  To get the custom location posts, we use:
 
<?php
  $args = array('post_type' => 'locations');
  $loop = new WP_Query($args);
  $locations = array();
  while ($loop->have_posts()) : $loop->the_post();
    $fields = get_fields();
    $fields['title'] = get_the_title();
    $locations[] = $fields;
  endwhile;
?>

Then we can use the following to output the data as a JSON object for our JavaScript.
 
<?php echo json_encode($locations);

3. Work JavaScript Magic

Now that we have the data available on to the JavaScript on the page, we can work now in JavaScript to render the map and locations. This example was created using the jQuery JavaScript library; any more complicated I would likely want to use the more structured AngularJS JavaScript library.

note: Again, I am assuming that the reader is proficient in JavaScript.

wp-content/themes/kss/page-locations.php
 
<?php while (have_posts()) : the_post(); ?>
  <?php get_template_part('templates/page', 'header'); ?>
  <?php get_template_part('templates/content', 'page'); ?>
<?php endwhile; ?>
<?php
  $args = array(
    'post_type' => 'locations',
    'nopaging' => 'true'
  );
  $loop = new WP_Query($args);
  $locations = array();
  while ($loop->have_posts()) : $loop->the_post();
    $fields = get_fields();
    $fields['title'] = get_the_title();
    $locations[] = $fields;
  endwhile;
?>
<div id="error"></div>
<div id="map" style="width: 100%; height: 400px;"></div>
<div id="locations"></div>
<script src="https://maps.googleapis.com/maps/api/js?key=OBMITTED"></script>
<script>
  (function() {
    var i, j, k, continents, conLocations, countries, couLocations;
    var $locations = jQuery('#locations');
    var $error = jQuery('#error');
    var locations = <?php echo json_encode($locations); ?>;

    // DISPLAY MAP
    var zoom = jQuery(window).width() < 768 ? 1 : 2;
    var mapOptions = {
      center: {
        lat : 25.3130239,
        lng : -117
      },
      zoom: zoom,
      disableDefaultUI: true,
      zoomControl: true,
      zoomControlOptions: {
        style: window.google.maps.ZoomControlStyle.DEFAULT,
        position: window.google.maps.ControlPosition.BOTTOM_LEFT
      },
      scaleControl: false,
      styles:
[{"featureType":"administrative","elementType":"labels.text.fill","stylers":[{"color":"#444444"}]},{"featureType":"landscape","elementType":"all","stylers":[{"color":"#f2f2f2"}]},{"featureType":"poi","elementType":"all","stylers":[{"visibility":"off"}]},{"featureType":"road","elementType":"all","stylers":[{"saturation":-100},{"lightness":45}]},{"featureType":"road.highway","elementType":"all","stylers":[{"visibility":"simplified"}]},{"featureType":"road.arterial","elementType":"labels.icon","stylers":[{"visibility":"off"}]},{"featureType":"transit","elementType":"all","stylers":[{"visibility":"off"}]},{"featureType":"water","elementType":"all","stylers":[{"color":"#46bcec"},{"visibility":"on"}]}]
  };
    var map = new window.google.maps.Map(window.document.getElementById('map'),
      mapOptions);

    // DISPLAY MARKERS
    for (i = 0; i < locations.length; i++) {
      locations[i].lat = Number(locations[i].lat);
      locations[i].lng = Number(locations[i].lng);
      try {
        new window.google.maps.Marker({
          position: locations[i],
          map: map
        });
      } catch(err) {
        $error.append('location with invalid latitude or longitude');
        return;
      }
    }

    // CONTINENTS
    var detail = [];
    detail.push('<ul class="continents">');
    continents = locations.map(getContinent);
    continents = jQuery.grep(continents, isFirstContinent).sort();
    for (i = 0; i < continents.length; i++) {
      detail.push('<li>');
      detail.push('<div class="continent-title">' + htmlEncode(continents[i]) + '</div>');
      conLocations = locations.filter(withContinent, i);

      // COUNTRIES
      countries = conLocations.map(getCountry);
      countries = jQuery.grep(countries, isFirstCountry).sort();
      detail.push('<ul class="countries">');
      for (j = 0; j < countries.length; j++) {
        detail.push('<li>');
        detail.push('<div class="country-title">' + htmlEncode(countries[j]) + '</div>');

        // LOCATIONS
        couLocations = conLocations.filter(withCountry, j);
        detail.push('<ul class="locations">');
        for (k = 0; k < couLocations.length; k++) {
          if (!/^[a-zA-Z]+$/.test(couLocations[k].type)) {
            $error.append('location with invalid type');
            return;
          }
          detail.push('<li class="' + htmlEncode(couLocations[k].type) + '">');
          detail.push('<div class="location-title">' + htmlEncode(couLocations[k].title) + '</div>');
          if (couLocations[k].address_1) {
            detail.push('<div class="address">' + htmlEncode(couLocations[k].address_1) + '</div>');
          }
          if (couLocations[k].address_2) {
            detail.push('<div class="address">' + htmlEncode(couLocations[k].address_2) + '</div>');
          }
          if (couLocations[k].address_3) {
            detail.push('<div class="address">' + htmlEncode(couLocations[k].address_3) + '</div>');
          }
          if (couLocations[k].phone) {
            detail.push('<div class="phone">' + htmlEncode(couLocations[k].phone) + '</div>');
          }
          detail.push('</li>');
        }
        detail.push('</ul>');
        detail.push('</li>');
      }
      detail.push('</ul>');
      detail.push('</li>');
    }
    detail.push('</ul>');
    $locations.append(detail.join('\n'));
    function htmlEncode(text) {
      return jQuery('<div/>').text(text).html();
    }
    function getContinent(location) {
      return location.continent;
    }
    function isFirstContinent(element, index) {
      return jQuery.inArray(element, continents) === index;
    }
    function withContinent(element) {
      return element.continent === continents[this];
    }
    function getCountry(location) {
      return location.country;
    }
    function isFirstCountry(element, index) {
      return jQuery.inArray(element, countries) === index;
    }
    function withCountry(element) {
      return element.country === countries[this];
    }
  })();
</script>

Tuesday, July 7, 2015

Going Multilingual

Turning on multilingual plug-ins do more than just allow one to manage multiple versions of a single page / post; rather they deeply integrate with WordPress allows for multiple menus, categories, etc.  WordPress provides a number of options for multilingual WordPress: https://codex.wordpress.org/Multilingual_WordPress; all of which require a non-trivial investment in time to implement.

In the case where one wants to implement a limited multilingual experience, e.g., only a selection of pages / posts need to be provided in multiple languages, one can leverage the Advanced Custom Forms Pro plugin to create a lightweight solution.

The general idea is:

1. Create a new field group, e.g., multi, with the structure:
  • Field: repeat of type Repeater.
    • Field: language of type Select choices:
      • English
      • EspaƱol
      • Arabic
    • Field content of type Wysiwyg Editor.
2. Create a new custom page template, e.g, by copying page.php, to page_multi.php, and add the following PHP comment to the top.

page_multi.php
<?php /* Template Name: Multi Template */ ?>

3. Change the multi field group to apply if Page Template is equal to Multi Template.

4. Now we update page_multi.php to use the fields in the field group multi as follows:

page_multi.php
<?php /* Template Name: Multi Template */ ?>
<?php get_header(); ?>
<?php wp_nav_menu(array('theme_location' => 'header-menu')); ?>
<?php if (function_exists('yoast_breadcrumb')) {
   yoast_breadcrumb();
 }?>
<?php while (have_posts()) : the_post(); ?>
  <?php while (have_rows('repeat')) : the_row(); ?>
      <div id="<?php the_sub_field('language'); ?>" class="multi" style="display: none;">
        <?php the_sub_field('content'); ?>
      </div>
  <?php endwhile; ?>
  <ul>
    <?php while (have_rows('repeat')) : the_row(); ?>
        <li><a onclick="show('<?php the_sub_field('language'); ?>')"><?php the_sub_field('language'); ?></a></li>
    <?php endwhile; ?>
  </ul>
<?php endwhile; ?>
<?php get_sidebar(); ?>
<?php get_footer(); ?>
<script>
  jQuery(document).ready(ready);
  function ready() {
    var language='English';
    var regex = /mylanguage=([^;]*)/;
    var match = document.cookie.match(regex);
    if (match) {
      language = match[1];
    } else {
      document.cookie='mylanguage=English; path=/';
    }
    show(language);
  }
  function show(language) {
    document.cookie='mylanguage=' + language + '; path=/';
    jQuery('.multi').hide();
    var content = jQuery('#' + language);
    if (content.length !== 0) {
      jQuery('#' + language).show();
    } else {
      jQuery('#English').show();
    }
  }
</script>

5. Finally, one can change a page's template to Multi Template to enable multiple languages for it.

Widgets: Outside My Comfort Zone

Did not take me long in WordPress to run into widgets; per the WordPress documentation:
Widgets were originally designed to provide a simple and easy-to-use way of giving design and structure control of the WordPress Theme to the user, which is now available on properly "widgetized" WordPress Themes to include the header, footer, and elsewhere in the WordPress design and structure. Widgets require no code experience or expertise. They can be added, removed, and rearranged on the WordPress Administration Appearance > Widgets panel. 
The use of widgets appears to be in conflict with my (evolving) philosophy of only using WordPress as a basic content management system; we shall see.

In order to get widgets support for a theme, one needs to implement a sidebar: https://codex.wordpress.org/Sidebars.

Extending on the Barebones theme, http://barebones-wp.blogspot.com/, we need to simply add the following:

functions.php
add_action('widgets_init','register_my_sidebars');
function register_my_sidebars() {
  register_sidebar(
    array(
      'id' => 'primary-sidebar',
      'name' => __('Primary Sidebar', 'barebones'),
      'before_widget' => '<div id="%1$s" class="%2$s">',
      'after_widget' => '</div>',
      'before_title' => '<h3>',
      'after_title' => '</h3>'
    )
  );
}

Then we implement a simple standard template file:

sidebar.php
<?php if (is_active_sidebar('primary-sidebar')) : ?>
  <?php dynamic_sidebar('primary-sidebar'); ?>
<?php endif; ?>

Then in each of the primary and secondary templates, e.g., page.php,  we need to pull in sidebar.php, e.g.,:

page.php
<?php get_sidebar(); ?>

Friday, July 3, 2015

Theme Bloat

An early decision in using WordPress is choosing a theme. Four examples will be highlighted: Barebones, Twenty Fifteen, BlankSlate, and Sage.

While there is details below, here is a summary of the files one is required to understand as WordPress renders a single static page for each of the themes. Non-standard (WordPress) files are highlighted in red.

Barebones Twenty Fifteen BlankSlate Sage
functions.php
header.php
style.css
Misc. Script/Styles*
page.php
footer.php
functions.php
header.php
style.css
Misc. Scripts/Styles*
page.php
content-page.php
footer.php
functions.php
header.php
style.css
Misc. Scripts /Styles*
page.php
footer.php
functions.php
8 custom PHP files
base.php
templates/head.php
Misc. Script/Styles
header.php**
templates/header.php
page.php
templates/page-header.php
templates/content-page.php
templates/content-footer.php


*note: While the actual Misc. Scripts/Styles themselves are not-standard, the implementation of including them (from functions.php) is.

**note: While header.php is attempted to load, the theme does not provide this file.

Barebones

One solution is to roll one's own theme, e.g., https://github.com/larkintuckerllc/barebones.

This example theme consists of
  • functions.php
  • style.css
  • seven of the eight (skipping comments-popup.php) primary templates
  • secondary template front-page.php.
  • header.php
  • footer.php
As for the number of files, there are 11 WordPress files (all documented by WordPress) and three small support files (for GIT and Bower).

To get a sense of the weight of the theme, here are the elements used to render a singular static page:

functions.php

Weighing in at 22 lines, the code is straight out of the WordPress documentation on enqueueing third-party scripts and registering menus. https://github.com/larkintuckerllc/barebones/blob/master/functions.php

header.php

At 6 lines, this is a minimal amount of HTML at the top; in reality one will want to add a minimal amount of additional meta information, e.g., "<!DOCTYPE html>".
https://github.com/larkintuckerllc/barebones/blob/master/header.php

style.css

This is essentially empty (in the example, just put in a sample selector to demonstrate that the file is being used).

Misc. Scripts and Styles

While not necessary, this theme includes the following popular elements as a demonstration as to how to incorporate third-party scripts.

  • jQuery (JavaScript)
  • Bootstrap (CSS and JavaScript)

page.php

At 9 lines, this simply injects a minimal navigation menu, breadcrumbs, and the page content.
https://github.com/larkintuckerllc/barebones/blob/master/page.php

footer.php

The 3 lines in this file simply pull in the WordPress footer and close out the HTML tags opened up in the header.php file.
https://github.com/larkintuckerllc/barebones/blob/master/footer.php

Twenty Fifteen

Twenty Fifteen is the default theme for WordPress in 2015,  https://codex.wordpress.org/Twenty_Fifteen.

This theme represents the polar opposite of Barebones in that it is designed to be used by novices to build out their site. The theme follows the WordPress documented structure of functions.php, styles.php, and standard template files.

Again, to get the sense of the weight, we review the elements used to build a singular static page.

functions.php

At 355 lines, it actually still is a fairly easy to follow implementation mostly consisting of WordPress function calls to setup things like enqueuing JavaScript, setting up menus, etc.
https://github.com/WordPress/WordPress/blob/master/wp-content/themes/twentyfifteen/functions.php

header.php

The 50 lines here is a fairly understandable implementation consisting of standard HTML meta information and some WordPress specific elements.
https://github.com/WordPress/WordPress/blob/master/wp-content/themes/twentyfifteen/header.php

style.css

Weighing in at 6002 lines, this definitely introduces significant opinionated structure to the theme.
https://github.com/WordPress/WordPress/blob/master/wp-content/themes/twentyfifteen/style.css

Misc. Scripts/Styles

In addition to to lengthy style.css, this theme includes:

  • Google Fonts (CSS)
  • Genericons (CSS)
  • ie.css (custom CSS)
  • ie7.css (custom CSS)
  • skip-link-focus-fix.js (custom JavaScript)
  • comment-reply (standard WordPress JavaScript)
  • keyboard-image-navigation.js (custom JavaScript)
  • functions.js (custom JavaScript)
page.php

This is a small 38 line standard implementation of the template file.


content-page.php

While not one of the WordPress standard template files, this short template file is common in WordPress themes.  Presumably, the purpose of this being in a separate file is to avoid having to repeat oneself as one builds out custom page templates.
https://github.com/WordPress/WordPress/blob/master/wp-content/themes/twentyfifteen/content-page.php

footer.php

Not much interesting here in the 34 lines.
https://github.com/WordPress/WordPress/blob/master/wp-content/themes/twentyfifteen/footer.php

BlankSlate

Like Barebones, this theme, https://github.com/tidythemes/blankslate, is designed to be a light unopinionated theme. The theme follows the WordPress documented structure of functions.phpstyles.php, and standard template files.

The elements used to build a singular static page.

functions.php

This is a smallish (66 lines) implementation of standard WordPress function calls to setup menus and such.
https://github.com/tidythemes/blankslate/blob/master/functions.php

header.php

The 24 lines here is a fairly understandable implementation consisting of standard HTML meta information and some WordPress specific elements.
https://github.com/tidythemes/blankslate/blob/master/header.php

style.php

Appears to include a single (long) selector designed as a CSS  reset.  Presumably would replace with own CSS reset or framework, e.g. Bootstrap.
https://github.com/tidythemes/blankslate/blob/master/style.css

Misc. Scripts/Styles

In addition to to lengthy style.css, this theme includes:

  • jQuery (JavaScript)

page.php

Short, 18 line, standard page template.
https://github.com/tidythemes/blankslate/blob/master/page.php

footer.php

Short, 11 line, standard footer template.
https://github.com/tidythemes/blankslate/blob/master/footer.php

Sage

In the same vein as Barebones and BlankSlate, Sage is designed to be a starting point for building out one's own WordPress template with a minimal opinion on style and structure.

However, it departs from the other three themes as it is very opinionated as to workflow. It seems to be part of a growing trend of software that pieces together a number of workflow tools to form an end-to-end workflow framework.

note: I tend to avoid such workflow frameworks as they tend to add their own layer of complication to what is already a fairly simple activity.

In addition to the opinionated workflow, Sage diverges from the standard WordPress templating strategy with their Theme Wrapper concept: https://roots.io/sage/docs/theme-wrapper/. This effort, in my opinion, is an overly complicated solution to a non-issue, i.e., having to include commands like "get_header()" in the WordPress templates.

For a singular static page, the following is used:

functions.php

This short 30 line file is a loader for 8 other custom php files consisting of some standard WordPress function calls, primarily init.php, and custom PHP functionality (the rest of the files).

base.php

This fairly short non-standard file fills the role of the header.php and footer.php (sort of) in more traditional themes. With a bit of PHP magic, this file wraps the standard WordPress template files; injecting their content into the main tag. The actual HTML is fairly standard and non-opinionated.
https://github.com/roots/sage/blob/master/base.php

templates/head.php

This is a non-standard file that renders the HTML head element; HTML is a short vanilla head.
https://github.com/roots/sage/blob/master/templates/head.php

Misc. Scripts/Styles

Sage uses a configuration file sage/assets/manifest.json with a complex gulpfule.js to prepare the CSS and JavaScript files for injection by the lib/assets.php (called from functions.php) file.

  • jQuery
  • modernizer
  • assets/scripts/main.js (custom JavaScript)
  • assets/styles/main.scss (custom CSS)
  • assets/styles/editor-style.scss (custom CSS)


templates/header.php

This is a non-standard file to render content just after the HTML body tag, e.g, banners and navigational menus.
https://github.com/roots/sage/blob/master/templates/header.php

note: Looks like base.php also attempt so find the standard header.php file and render it into the body  above the header.php (not included in the theme).

page.php

This is the standard page.php file that gets injected into the main section of base.php; basically used to include two non-standard files.
https://github.com/roots/sage/blob/master/page.php

templates/page-header.php

This is a non-standard file to display the top part of pages, e.g., custom page templates, 404.php, etc.
https://github.com/roots/sage/blob/master/templates/page-header.php

templates/content-page.php

This is a non-standard file that simply puts the content on the page.
https://github.com/roots/sage/blob/master/templates/content-page.php

templates/footer.php

This is a non-standard file that outputs a footer section on the page.
https://github.com/roots/sage/blob/master/templates/footer.php

The Slippery Slope

In two earlier blog formatted articles, I had explored ideas around using WordPress in a professional manner:
MEAN Stack and WordPress Marriage
http://mean-wordpress.blogspot.com/
This series of posts starts with the problem: I, as others that I know, have been struggling to figure out a good solution for handling user-managed content embedded in a MEAN (MongoDB, Express, AngularJS, and Node.js) stack application.
It ends with the conclusion that WordPress is an excellent choice for a CMS component. The caveat, however, it steers one against using WordPress as a general development platform. 
Barebones WordPress
http://barebones-wp.blogspot.com/
This series of posts explores the basics of creating WordPress themes for one's use; as opposed to creating one for redistribution.  The distinction is important as we will only focus on developing it for a particular use and thus greatly simplify the effort.
This effort also is focused on giving one maximal control over the site (HTML, CSS, and JavaScript) with minimal use of programming (WordPress and PHP).
In this series of posts, we will explore the slippery slope of using WordPress and the temptation to overly depend on prebuilt components (themes and plugins) and then begin using it as a general development platform. We will contrast this approach with a more lean strategy that I have been advocating.

This experience is born out of a project where speed was of the essence and the taking a more traditional theme and plugin heavy approach was our team's joint solution (principally because of the familiarity of this approach given the timeline).