Track Ajax Pages with Google Analytics

Track Ajax Pages with Google Analytics

[original: https://mjau-mjau.com/blog/ajax-universal-analytics/]

Single Page Applications

Modern websites often use html5 pushstate and ajax to load content dynamically, providing a fluid user experience and improved performance. Such websites are often refered to as single page applications (SPA). Unlike traditional websites that load the analytics tracker on each full page load, SPA’s require us to track virtual page views manually.

Google Universal Analytics

This tutorial is written in regards to Google Universal Analytics, launched in 2014, effectively replacing Classic Google Analytics. I am writing this guide because I could not find much information about using Universal Analytics in SPA’s. Benefits of upgrading from Classic- to Universal Google Analytics, can be found here.

Default Tracking Code

For Universal Analytics, you are asked to copy and paste the following tracking code into every webpage you want to track. Basically, it injects the analytics script asynchronously into the page, initiates the tracker and tracks the page that loads:

  (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
  (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
  m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
  })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
  ga('create', 'UA-XXXX-Y', 'auto');
  ga('send', 'pageview');

Ref.: Introduction to Analytics.js

Custom Tracking Code

What if we don’t want inline javascript junk floating around in our documents? What if we want to initiate analytics and track virtual pageviews from within our own javascript application? The tracking code can easily be converted into a custom function readable to humans:

function gaTracker(id){
  $.getScript('//www.google-analytics.com/analytics.js'); // jQuery shortcut
  window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
  ga('create', id, 'auto');
  ga('send', 'pageview');   
}

Some would argue that this could delay tracking of the initial pageview, but personally I prefer to focus client resources on loading the native website application before applying unrelated resources. Furthermore, adding the tracker directly to our javascript application, makes sure we don’t need to bloat page documents with the default tracking code.

Ref.: Analytics Advanced Configuration

Tracking Virtual Page Views

After initiating the tracker and loading virtual pages with our Ajax/html5 application, we can easily track each page by triggering the following analytics method:

ga('send', 'pageview');

The above will automatically use the path segment in the browser address bar, which you have likely updated already either with html5 pushstate or #hash. Personally, I prefer to wrap the page tracker inside my own function for additonal control:

function gaTrack(path, title) {
  ga('set', { page: path, title: title });
  ga('send', 'pageview');
}

The advantage of the above, is that it allows us to persistently set the page path and title for the current page. This is beneficial as it allows us to trigger the tracker even if the target page has not yet loaded. Also, it allows us to set page data for multiple hits, for instance if we want to track image clicks from within the same page.

Ref.: Single Page Application Tracking

Code summary

Javascript
// Function to load and initiate the Analytics tracker
function gaTracker(id){
  $.getScript('//www.google-analytics.com/analytics.js'); // jQuery shortcut
  window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
  ga('create', id, 'auto');
  ga('send', 'pageview');
}

// If you are not using jQuery, replace $.getScript() line above with the following:
var script = document.createElement('script');
script.src = '//www.google-analytics.com/ga.js';
document.getElementsByTagName('head')[0].appendChild(script);

// Function to track a virtual page view
function gaTrack(path, title) {
  ga('set', { page: path, title: title });
  ga('send', 'pageview');
}

// Initiate the tracker after app has loaded
gaTracker('UA-XXXX-Y');

// Track a virtual page
gaTrack('/silence/golden/', 'Silence is Golden');

// If you don't need to set page path and title, skip the _gaTracker function, and use the native analytics page tracker instead:
ga('send', 'pageview')

Bonus: Track image clicks

On the topic of dynamic tracking, you might want to know how to track an image click event also:

// Track an image click event
ga('send', 'event', 'image', 'click', 'image click', 'filename.jpg');
// ga('send', 'event', 'category', 'action', 'label', 'value');

Ref.: Event Tracking

Use WordPress Contact Form to generate Leads in Sales Force or where you want

Use WordPress Contact Form to generate Leads in Sales Force

I really love CF7, it is the best solution to produce and generate forms without mess.

A lot of time you need a form not to send email but to profile users or store data.

You can use CF7 adding a lille bit of code.

add_action( 'wpcf7_before_send_mail', 'myfunction' );

Inside the action you are able to intercept the CF7 object and the global CF7 instance.

function myfunction( $cf7 ){ }

Inside the myfunction you can check the $cf7 object with its posted data

$email = $cf7->posted_data["your-email-field-name"];

personally i use to intercept the mail CF7 instance

$cf7_submission = WPCF7_Submission::get_instance();

if ( $cf7_submission ) {
   $cf7_data = $cf7_submission->get_posted_data()
}

it returns a simple associative array with all de posted data i need

Array
(
 [_wpcf7] => 602
 [_wpcf7_version] => 4.5
 [_wpcf7_locale] => it_IT
 [_wpcf7_unit_tag] => wpcf7-f602-p258-o1
 [someCustomVar1] => somePostedCustomVarValue1
 [someCustomVar2] => somePostedCustomVarValue2
 [someCustomVar3] => somePostedCustomVarValue3
 ...
 [_wpcf7_is_ajax_call] => 1
)

It is fun, you can define the “someCustomVars” also writing them as hidden input (if you need) in the CF7 editor.

The integration with SalesForce it is easy you need to collect the posted vars, and send them via curl to the SalesForce account.

Some code is following.

What you can obtain from $cf7 object from inside the action

WPCF7_ContactForm Object
(
 [id:WPCF7_ContactForm:private] => 602
 [name:WPCF7_ContactForm:private] => form_id
 [title:WPCF7_ContactForm:private] => Form Name
 [properties:WPCF7_ContactForm:private] => Array
 (
 [form] => FORM CODE FORM THE CF7 EDITOR (is html)
 [mail] => Array
 (
 [subject] => subject
 [sender] => [first_name] <wordpress@domain.com>
 [body] => From: [first_name] <[email]>
 Subject: mail from website

 Data sent by email:
 [company]
 [first_name]
 [last_name]
 [recipient] => DESTINATION EMAIL
 [additional_headers] => Reply-To: [email]
 [attachments] => 
 [use_html] => 1
 [exclude_blank] => 
 )

 [mail_2] => Array
 (
 [active] => STATUS
 [subject] => Subject "[your-subject]"
 [sender] => Sender <wordpress@domain.com>
 [body] => From: [first_name] <[email]>
 Subject: mail from website

 Data sent by email:
 [company]
 [first_name]
 [last_name]
 [recipient] => [your-email]
 [additional_headers] => Reply-To: email
 [attachments] => 
 [use_html] => 
 [exclude_blank] => 
 )

 [messages] => Array
 (
 [mail_sent_ok] => Your message was sent successfully.
 [mail_sent_ng] => Failed to send your message.
 [validation_error] => Validation errors occurred. Please confirm the fields and submit it again.
 [spam] => This submission has been marked as spam. Please try again.
 [accept_terms] => You must agree to the Terms and Conditions before sending your message.
 [invalid_required] => The field is required.
 [invalid_too_long] => The field is longer than the maximum allowed length.
 [invalid_too_short] => The field is shorter than the minimum allowed length.
 [invalid_date] => The date you entered in this field is invalid.
 [date_too_early] => The date you entered is earlier than minimum limit.
 [date_too_late] => The date you entered is later than maximum limit.
 [upload_failed] => An unknown error occurred while uploading the file to the server.
 [upload_file_type_invalid] => This file type is not supported.
 [upload_file_too_large] => The file uploaded is too large.
 [upload_failed_php_error] => A PHP errorerror occured while uploading file to the server.
 [invalid_number] => The number format entered is invalid
 [number_too_small] => The number is smaller than minimum limit
 [number_too_large] => The number is larger than maximum limit
 [quiz_answer_not_correct] => Your answer is not correct
 [captcha_not_match] => Il codice che hai inserito non è valido.
 [invalid_email] => The email address entered is invalid
 [invalid_url] => URL is invalid
 [invalid_tel] => The telephone mumber you entered is invalid
 )

 [additional_settings] => 
 )

 [unit_tag:WPCF7_ContactForm:private] => 
 [responses_count:WPCF7_ContactForm:private] => 0
 [scanned_form_tags:WPCF7_ContactForm:private] => Array
 (
 A BIG ARRAY WITH THE DEFINITION OF ALL FILEDS GENERATED WITH CF7 TAGS

 )

 [locale] => YOUR LANG CODE
)

What you can obtain from CF7 instance from inside the action

Array
(
 [_wpcf7] => 602
 [_wpcf7_version] => 4.5
 [_wpcf7_locale] => it_IT
 [_wpcf7_unit_tag] => wpcf7-f602-p258-o1
 [someCustomVar1] => somePostedCustomVarValue1
 [someCustomVar2] => somePostedCustomVarValue2
 [someCustomVar3] => somePostedCustomVarValue3
 ...
 [_wpcf7_is_ajax_call] => 1
)

An example to integrate CF7 with SalesForce (or anything else)

On the CF7 editor

<input type="hidden" id="usesf" name="usesf" value="true">
<input type="hidden" id="oid" name="oid" value="XXX">
<input type="hidden" id="lead_source" name="lead_source" value="Web" />
<input type="hidden" id="baseaction" name="baseaction" value="https://www.salesforce.com/servlet/servlet.WebToLead?encoding=UTF-8" />

On the functions.php

add_action( 'wpcf7_before_send_mail', 'my_lead_generator_conversion' );
function my_lead_generator_conversion( $cf7 ){

 $arwv_submission = WPCF7_Submission::get_instance();

 if ( $arwv_submission ) {

 //inizio registrazione
 $arwv_data = $arwv_submission->get_posted_data();

 if($arwv_data['usesf'] == true || $arwv_data['usesf'] == 'true'){

 //collecting data for salesforce
 $sfdata_usesf = $arwv_data['usesf'];
 $sfdata_oid = $arwv_data['oid'];
 $sfdata_lead_source = $arwv_data['lead_source'];
 $sfdata_baseaction = $arwv_data['baseaction'];

 //collecting userdata
 $sfudata_company = $arwv_data['company'];
 $sfudata_first_name = $arwv_data['first_name'];
 $sfudata_last_name = $arwv_data['last_name'];
 $sfudata_email = $arwv_data['email'];

 $post_items[] = 'oid='.$sfdata_oid;
 $post_items[] = 'company='.$sfudata_company;
 $post_items[] = 'first_name='.$sfudata_first_name;
 $post_items[] = 'last_name='.$sfudata_last_name;
 $post_items[] = 'email='.$sfudata_email;

 if(!empty($sfudata_last_name) && !empty($sfudata_first_name) && !empty($sfudata_email) ){
 $post_string = implode ('&', $post_items);

 // Create a new cURL resource
 $ch = curl_init();

 if (curl_error($ch) != ""){
 //errors
 }

 $con_url = $sfdata_baseaction;
 curl_setopt($ch, CURLOPT_URL, $con_url);

 // Set the method to POST
 curl_setopt($ch, CURLOPT_POST, 1);

 // Pass POST data
 curl_setopt( $ch, CURLOPT_POSTFIELDS, $post_string);
 curl_exec($ch); // Post to Salesforce
 curl_close($ch); // close cURL resource

 }

 }

 }

 return $cf7;
}

Synchronise WordPress Uploads Across Environments

Synchronise WordPress Uploads Across Environments

Read it at http://www.polevaultweb.com/2014/03/5-ways-synchronise-wordpress-uploads-across-environments/

rsync

I was recently introduced to rsync to help copy only the missing images between environments. Wikipedia describes it well:

rsync is a utility software and network protocol for Unix-like systems (with a port to Microsoft Windows) that synchronizes files and directories from one location to another while minimizing data transfer by using delta encoding when appropriate.

rysnc needs to be run in the command line and connecting to your remote site needs to be with SSH.

First of all navigate to your local WordPress site:
cd wordpress-site

Move to your uploads folder:
cd wp-content/uploads

To perform the synchronisation (replacing the SSH credentials):
rsync -avz --rsh=ssh user@host:/path/to/site/wp-content/uploads/* .

rsync is a lot quicker as it uses an SSH connection and is a very powerful tool. Read more about the different options and configuration here.

 .htaccess

The last two solutions are my favourites as they don’t require any form of copying of files across environments.

This involves adding an .htaccess file to the wp-content/uploads/ directory of your site with this code:

<IfModule mod_rewrite.c>
  RewriteEngine On

  RewriteBase /wp-content/uploads/
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule ^(.*) http://yourlivesite.com/wp-content/uploads/$1 [L,P]

</IfModule>

So for any file requested within wp-content/uploads/, that does not exist, it will serve the image from http://yourlivesite.com.

You may need to edit the RewriteBase to suit your site’s base path in your various environments.

WordPress Filters

This  is the equivalent of the .htaccess code but using core filters to serve the images. You need to add some extra config to your site’s wp-config.php:

Define your site’s url used by WP:

define('WP_SITEURL', 'http://' . $_SERVER['HTTP_HOST'] .
    str_replace(DIRECTORY_SEPARATOR, '/', str_replace(realpath($_SERVER['DOCUMENT_ROOT']), '', dirname(__FILE__))));

Define the url of your production site where most up to date media will be uploaded:

define('LIVE_SITEURL', 'http://yourlivesite.com');

Then in your theme’s functions.php, a plugin or better still a site-wide functions plugin, add:

add_action('init', 'my_replace_image_urls' );
function my_replace_image_urls() {
    if ( defined('WP_SITEURL') && defined('LIVE_SITEURL') ) {
        if ( WP_SITEURL != LIVE_SITEURL ){
            add_filter('wp_get_attachment_url', 'my_wp_get_attachment_url', 10, 2 );
        }
    }
}

function my_wp_get_attachment_url( $url, $post_id) {
    if ( $file = get_post_meta( $post_id, '_wp_attached_file', true) ) {
        if ( ($uploads = wp_upload_dir()) && false === $uploads['error'] ) {
            if ( file_exists( $uploads['basedir'] .'/'. $file ) ) {
                return $url;
            }
        }
    }
    return str_replace( WP_SITEURL, LIVE_SITEURL, $url );
}

This will work for uploaded media that is being served by WordPress’ standard image functions such as the_post_thumbnail() or wp_get_attachment_image().

The function my_wp_get_attachment_url() does the work, but will only be added if it is not the live site, as it will already have all the images.

The function then hooks into the WordPress filter wp_get_attachment_url, so for each time WordPress is serving an attachment url our function jumps in and checks if that attachment file exists in our site, and if not swaps out the site url with the live url.

Image Mask Effect

Image Mask Effect

Go to Source
image-mask-effect

An immersive transition effect powered by image masks and CSS transforms.

We’ve been publishing in our library some nice mask effects powered by SVG properties. This time we took advantage of the PNG transparencies to zoom through the mask layer, into a project background image.

If you want to change the color of the .pgn masks, you can easily do it in Photoshop (or any other graphic tool) by applying a color overlay to the whole image layer. If you plan to create your own masks, please note that this effect works only if there’s an empty space in the very center of the mask.

Inspiration: Offsite Homepage Animation by Hrvoje Grubisic.

Creating the structure

The HTML structure is composed of a list of <section>s wrapped in a .cd-image-mask-effect element. Each <section> contains a div.featured-image (project image), a div.mask (image mask) and a div.cd-project-info for the project content.

<section class="project-1 cd-project-mask">
	<h1>Project Name</h1>
	<div class="featured-image"></div>
	<div class="mask">
		<img src="img/mask-01.png" alt="mask">
		<span class="mask-border mask-border-top"></span>
		<span class="mask-border mask-border-bottom"></span>
		<span class="mask-border mask-border-left"></span>
		<span class="mask-border mask-border-right"></span>
	</div>

	<a href="#0" class="project-trigger">Explore Project</a>

	<a href="#0" class="cd-scroll cd-img-replace">Scroll down</a>

	<div class="cd-project-info" data-url="project-1">
		<!-- content loaded using js -->
	</div>

	<a href="#0" class="project-close cd-img-replace">Close Project</a>
</section> <!-- .cd-project-mask -->

<section class="project-2 cd-project-mask">
	<!-- content here -->
</section>

<!-- other sections here -->

The project content is not included in the HTML but is loaded using JavaScript.

Adding style

Each .cd-project-mask has a height of 100vh (viewport height) and a width of 100%; the project image is set as background-image of the .featured-image element, while the mask image is wrapped inside the .mask element.
Four .mask-border elements have been used to create a frame around the image mask to make sure the project featured image is not visible outside the mask (we used <span> elements rather than pseudo elements because their behaviour was buggy on Safari 9).

.cd-project-mask {
  position: relative;
  height: 100vh;
  width: 100%;
  overflow: hidden;
}

.cd-project-mask .featured-image {
    /* project intro image */
    position: absolute;
    left: 50%;
    top: 50%;
    bottom: auto;
    right: auto;
    transform: translateX(-50%) translateY(-50%);
  	height: 100%;
  	width: 100%;
  	background: url(../img/img-01.jpg) no-repeat center center;
  	background-size: cover;
}

.cd-project-mask .mask {
  position: absolute;
  left: 50%;
  top: 50%;
  bottom: auto;
  right: auto;
  transform: translateX(-50%) translateY(-50%);
  width: 300px;
  height: 300px;
}

.cd-project-mask .mask .mask-border {
  /* this is used to create a frame around the mask */
  position: absolute;
}

.cd-project-mask .mask .mask-border-top,
.cd-project-mask .mask .mask-border-bottom {
    /* this is used to create a frame around the mask */
    height: calc(50vh - 150px + 10px);
    width: 100vw;
    left: 50%;
    right: auto;
    transform: translateX(-50%);
}

.cd-project-mask .mask .mask-border-top {
    bottom: calc(100% - 10px);
}

.cd-project-mask .mask .mask-border-bottom {
    top: calc(100% - 10px);
}

.cd-project-mask .mask .mask-border-left,
.cd-project-mask .mask .mask-border-right {
  	/* this is used to create a frame around the mask */
	height: 100vh;
	width: calc(50vw - 150px + 10px);
	top: 50%;
	bottom: auto;
    transform: translateY(-50%);
}

.cd-project-mask .mask .mask-border-left {
  left: calc(100% - 10px);
}

.cd-project-mask .mask .mask-border-right {
  right: calc(100% - 10px);
}

When the user selects a project, the class .project-view (added to the wrapper .cd-image-mask-effect) is used to hide all the other projects.
The .mask element is then scaled up to reveal the project featured image and the project content is loaded (more in the Events handling section).

.project-view .cd-project-mask:not(.project-selected) {
   /* the project-view class is added to the .cd-image-mask-effect element when a project is selected - hide all not selected projects */
   position: absolute;
   top: 0;
   left: 0;
   opacity: 0;
   visibility: hidden;
}

Events handling

To implement this image mask effect, we created a ProjectMask object and used the initProject method to attach the proper event handlers.

function ProjectMask( element ) {
	this.element = element;
	this.projectTrigger = this.element.find('.project-trigger');
	this.projectClose = this.element.find('.project-close'); 
	this.projectTitle = this.element.find('h1');
	this.projectMask = this.element.find('.mask');
	//...
	this.initProject();
}

var revealingProjects = $('.cd-project-mask');
var objProjectMasks = [];

if( revealingProjects.length > 0 ) {
	revealingProjects.each(function(){
		//create ProjectMask objects
		objProjectMasks.push(new ProjectMask($(this)));
	});
}

When the user selects a project, the revealProject method is used to scale up the mask image while the uploadContent method takes care of loading the project content (using the load() function) and adding the new page to the window.history (using the pushState() method).

ProjectMask.prototype.initProject = function() {
	var self = this;

	//open the project
	this.projectTrigger.on('click', function(event){
		event.preventDefault();
		if( !self.animating ) {
			self.animating = true;
			//upload project content
			self.uploadContent();
			//show project content and scale up mask
			self.revealProject();
		}
	});

	//...
};

ProjectMask.prototype.revealProject = function() {
	var self = this;
	//get mask scale value
	self.updateMaskScale();
	//scale up mask and animate project title
	self.projectTitle.attr('style', 'opacity: 0;');
	self.projectMask.css('transform', 'translateX(-50%) translateY(-50%) scale('+self.maskScaleValue+')').one(transitionEnd, function(){
		self.element.addClass('center-title');
		self.projectTitle.attr('style', '');
		self.animating = false;
	});

	//hide the other sections
	self.element.addClass('project-selected content-visible').parent('.cd-image-mask-effect').addClass('project-view');
}

ProjectMask.prototype.uploadContent = function(){
	var self = this;
	//if content has not been loaded -> load it
	if( self.projectContent.find('.content-wrapper').length == 0 ) self.projectContent.load(self.projectContentUrl+'.html .cd-project-info > *');
	
	if( self.projectContentUrl+'.html'!=window.location ){
        //add the new page to the window.history
        window.history.pushState({path: self.projectContentUrl+'.html'},'',self.projectContentUrl+'.html');
    }
}

 
Go to Source