Gallery Captions for WooCommerce 1.10 – Maintenance & Compatibility Release

After a long pause in releasing, I just got Gallery Captions for WooCommerce version 1.10 up on WooCommerce.com. This update brings compatibility with YITH WooCommerce Product Gallery & Image Zoom, changes the way compatibility code is loaded, and bumps the “tested up to” versions of WordPress and WooCommerce to the most recent versions.

Because the last release was in late 2023, this release was my first time using WooCommerce’s Quality Insight Tests. My first impressions: they take longer than the old tests, but give more detailed feedback – I like them!

If you want to have captions under the images in your WooCommerce store, and have somehow landed here before WooCommerce.com, head on over and install Gallery Captions for WooCommerce. It has great reviews and I provide great support!

Gallery Captions 1.6: Now with Custom HTML and Thumbnail Captions

Version 1.6 of my WooCommerce extension, Gallery Captions for WooCommerce, is now live with two new features: HTML is allowed in captions and image thumbnails can now have captions, (if you really want them to).

Basic HTML in Captions

Some support requests have come in since the launch of Gallery Captions for WooCommerce asking how to add links, and occasionally other formatting, to captions. Now people no longer have to ask, they can just add HTML. I’m using the built-in $allowedtags variable in WordPress to allow most formatting tags but not big structural tags that have the potential to break the product image gallery.

Captions on Thumbnails

A while ago there was a support request asking to for a way to add captions to the thumbnails displayed below the full-size image in the product gallery, and instead of saying that’s not how the extension works I made it work and gave the customer a development version of Gallery Captions with captions on the thumbnails with the version number 1.6-dev. In order to release 1.6 without breaking that customer’s site when the extension updates I added thumbnail captions to the release version, but hid it behind a filter. Most of the time it doesn’t make sense to have captions on thumbnails, but when it does site owners can now filter gcw_show_thumbnail_captions to return true and they’ll get thumbnail captions, like this:

add_filter( 'gcw_show_thumbnail_captions', '__return_true' );
A screenshot showing the a WooCommerce product page with captions under the thumbnails. The captions are used to describe the colours of tulips.
Very short captions look pretty good under the thumbnails, but if the captions are long it looks pretty weird.

Both of these changes have the potential to cause trouble by letting people break the image gallery in unexpected ways, so hopefully I’m not making a mistake. Putting thumbnail captions behind a filter that should restrict the use of thumbnail captions to people who really know what they’re doing, and limiting the amount of HTML allowed in captions should prevent the worst disasters, and I can always limit it more in the future if needed.

Adding Google Ads and Microsoft Advertising Conversion Tags to WooCommerce

Adding Google Ads and Microsoft Advertising conversion tracking tags to WooCommerce powered E-commerce shops can seem confusing at first: WooCommerce templates are like a huge Russian nesting doll and it’s unclear where to put the tags. On top of that Google wants their tags as close to the top of the document as possible, but many tutorials suggest using the woocommerce_thankyou hook, which dumps its output right in the middle of the body.

Use Any WordPress Hook

Luckily we can just use WordPress hooks to insert conversion tags where we need them, so long as we make sure we’re on the “Thanks for your order” page, and check that the order is a status that we want to count as a conversion. Here’s how we can do that:

/**
 * This function can be hooked onto any normal WordPress hook. Maybe wp_head,
 * or wp_footer, or even a sidebar if you really want.
 * @return [type] [description]
 */
function jb_do_stuff_on_order_complete_page() {
	// is_order_received_page() is part of WooCommerce
	// if there's a possibility that the page is loaded without WooCommerce
	// you could check to make sure it exists:
	if(!function_exists('is_order_received_page')) return;

	if(is_order_received_page()) {
		global $wp;
		$order = wc_get_order($wp->query_vars['order-received']);
		if($order && !$order->has_status( 'failed' )) {
			// Do what we need here.
		}
	}	
}

Set Up Conversion Tracking

Both Google and Microsoft have two parts to their tracking tag: a global tag that goes on every page of the website, (Microsoft calls this a UET tag, for “Universal Event Tracking“), and a conversion snippet. Both companies ask that that the global tag appears first in the document’s source. For Google this is because the gtag() function used by the conversion snippet is defined by the global tag.

Google Ads

Google really likes their global tag to be as high in the page’s source as possible – right after the opening <head> tag if possible. Because of this I often put the global tag right into header.php. A more portable solution is to use the wp_head hook so we’ll do that here:


function jb_set_up_google_tags() {

	// Start with the global tag.
?>
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXX-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());
  gtag('config', 'UA-XXXXXXX-1');	// sets up Google Analytics - Google will give you the exact value
  gtag('config', 'AW-XXXXXXX');		// added for the conversion - Google will give you the exact value

  if (window.performance) {
    var timeSincePageLoad = Math.round(performance.now());
    gtag('event', 'timing_complete', {
      'name': 'load',
      'value': timeSincePageLoad,
      'event_category': 'JS Dependencies'
    });
  }
</script>
<?php

	// Add the conversion tag IF this is an Order Complete page.
	if(is_order_received_page()) {
		global $wp;
		$order = wc_get_order($wp->query_vars['order-received']);
		if($order && !$order->has_status( 'failed' )) {
?>		
<!-- Event snippet for Sales conversion page -->
<script>
  gtag('event', 'conversion', {
      'send_to': 'AW-XXXXXXX/XXXXXXXXX__XXXXX',		// Google will provide this value.
      'value': <?php echo $order->get_subtotal(); ?>,
      'currency': 'USD',							// Change you deal with a different currency. Or set it dynamically.
      'transaction_id': '<?php echo $order->get_order_number(); ?>'
  }); 
</script>
<?php
		}
	}
}

// Set priority 1 to appear as soon as possible in the wp_head.
add_action( 'wp_head', 'jb_set_up_google_tags', 1 );

Boom! That’s it for Google.

Microsoft Advertising

Microsoft advertising is very similar, except they seem to suggest that they want the conversion snippet before the closing </body> tag, and don’t seem to mind about the UET tag, so let’s just put them both in the wp_footer:

function jb_add_microsoft_advertising_tags() {

	// Start with the UET tag that goes on every page.
?>
<script>(function(w,d,t,r,u){var f,n,i;w[u]=w[u]||[],f=function(){var o={ti:"XXXXXXX"};o.q=w[u],w[u]=new UET(o),w[u].push("pageLoad")},n=d.createElement(t),n.src=r,n.async=1,n.onload=n.onreadystatechange=function(){var s=this.readyState;s&&s!=="loaded"&&s!=="complete"||(f(),n.onload=n.onreadystatechange=null)},i=d.getElementsByTagName(t)[0],i.parentNode.insertBefore(n,i)})(window,document,"script","//bat.bing.com/bat.js","uetq");</script>
<?php

	// Then add the conversion tag IF this is an Order Complete page.
	if(is_order_received_page()) {
		global $wp;
		$order = wc_get_order($wp->query_vars['order-received']);
		if($order && !$order->has_status( 'failed' )) {

			$order_info = [
				'revenue_value' => $order->get_subtotal(),
				'currency' => 'USD'
			];
?>
<script>
   window.uetq = window.uetq || [];
   window.uetq.push('event', '', <?php echo json_encode($order_info); ?>);  
</script>
<?php
		}
	} // end if this is an order received page.		

}
add_action( 'wp_footer', 'jb_add_microsoft_advertising_tags', 90 ); // Priority 90 to appear near the end.

Microsoft’s documentation on adding the conversion snippet is somewhat confusing. It talks about adding extra Javascript functions to supply the conversion value and doesn’t say what data type the conversion value should be, (let me know if you know). A Javascript function to extract the conversion value from the DOM might be a good idea when there’s no server-side access to the purchase data, but if you’re developing a WooCommerce theme or plugin you have the access you need to just print the conversion value in the tag with PHP, (as we did above).

Conclusion

That’s it. I heard that some people have trouble getting Bing working, (probably because of the extra-confusing directions from Microsoft), and I had trouble finding resources on adding conversion tags to the header for Google Ads. Hopefully this helps people with one, (or both), of the same questions.

WP e-Commerce to WooCommerce

There’s a pair of websites that I have looked after for a long time. They belong to a company that sells a few products, and have had quite a few sales over the years. This fall we’re doing some major renovations, and one of the things being changed is the underlying E-Commerce plugin for WordPress; we are changing from WP e-Commerce to WooCommerce. I expected that after installing WooCommerce it would offer to convert my WPeC store into a WooCommerce store. I was wrong. There was an official converter plugin but it was abandoned long ago.

Options to change a site from WP e-Commerce to WooCommerce

It looks like there are three ways to change a site from WP e-Commerce to WooCommerce:

  1. Use a commercial service like Cart2Cart. Cart2Cart looks great, but is made to move a store from one site to another, and I need an in-place conversion. They also charge per-migration, so if I want to migrate my localhost, modify the theme for WooCommerce, then migrate the live site, I’ll have to pay 1.5 times, (they give a 50% discount for re-migrating a site). With two sites my bill would have been around US$450, high enough for me to look at other, more complicated options.
  2. Create or update something like the old WooCommerce migration plugin.
  3. Manual Migration: This would involve a lot of SQL.

Cart2Cart looks ill-suited for my use-case, and expensive. Manual migration looks difficult, highly prone to errors, and not easily repeatable. This leaves creating or updating a migration utility. This migration script by Carl Hughes is bit newer than the WooCommerce one, promises to do more out of the box, and quick read-through doesn’t show anything too mysterious. It was my starting point.

Updating and Testing

It took three times as long as expected to update & test the migration plugin. Here’s what I learned, for anyone else that might be considering the same type of process:

  • This should go without saying, but this migration is destructive. If it fails the database will be in a strange state between WPeC and WooCommerce. Have a backup.
  • Since migrating from WPeC to WooCommerce changes the database, but only the database, (no media files are touched), a backup of the WordPress database is required. A lot of time will be spent reloading from the backup, so put it somewhere easy to get. Mine was on my desktop. Bonus points: a short bash script to reload the DB from the backup will save development time.
  • Make the backup be the exact state that the migration should run from. It is not efficient to disable a plugin in the WP Admin every time the database is reloaded.
  • Some plugins really slow down the migration. The most obvious ones are WP e-Commerce itself, and any Varnish caching plugin. If they are disabled the migration will run faster.
  • Each payment gateway, (plugin for a payment processor, in WPeC terminology), stores transaction data a little differently. I added the ability to port data from Authorize.Net credit card transactions, but separate routines need to be written for other processors.

Updated Migration Plugin

My fork of the migration plugin is up on Github, and a I sent a pull request to the original author so he can include my work if he wants. If you need to migrate from WP e-Commerce to WooCommerce hopefully this can help a bit. Read the readme. Migrating is harder than it should be, but it is possible.

Changing the Auto-Suggest Behaviour in WooCommerce

A request came in this week from a client with a WooCommerce store:

I just placed an order through our website and had an issue with the state. The customer lives in Oregon, so I typed “O” hoping Oregon would auto populate, but it didn’t. I was stuck having to scroll through all 50 states to find Oregon.

An animated Gif showing WooCommerce's default autosuggest behaviour on select boxes.
Confusing for most people.

I tried it, and it’s true. Typing “o” doesn’t narrow the list down much, and the way it does narrow it down doesn’t make much sense in this context: The autosuggest on the State field matches the search phrase anywhere in one of the options, so typing “o” gets you a list of all states with the letter “o” in the state name, ordered alphabetically by state name. Go ahead and try it out in a WooCommerce demo shop.

Out of the box WooCommerce uses SelectWoo, which is essentially Select2 with more accessibility. Select2’s default behaviour of matching the typed text to any part of an option makes sense in many places, especially when searching a store or for categories, but for selecting states, (and countries, although slightly less), it’s confusing.

Nothing’s filterable, but we can set a default

Select2 lets us set the callback function used for matching, so we can customize the matching behaviour, but there’s a problem, the settings for SelectWoo are set in WooCommerce’s country-select.js file, and there’s no way to filter them, so we can’t change the matching behaviour on initialization. It turns out that once a Select2/SelectWoo instance is initialized there’s no way to change the settings either.

When I got to here I thought we were out of luck – either I’d be creating a child theme and re-implementing the checkout, or we were going to live with SelectWoo’s default filtering behaviour. Luckily, I found one way to change the matching callback: It’s possible to set the default SelectWoo options callback before any instances are initialized. So that’s what we’re going to do.

Server-Side

First we need to add a bit of Javascript to our checkout page, so we enqueue a small script that’s dependent on the selectWoo script.


namespace JB\WooAutosuggest;



function enqueue_frontend_scripts() {
    
    if( function_exists('is_checkout') && is_checkout() ) {

        wp_enqueue_script( 'jb-was-checkout-autosuggest', plugins_url( '../assets/checkout-autosuggest.js', __FILE__), array('selectWoo') );

    }
}

add_action( 'wp_enqueue_scripts', __NAMESPACE__ . '\enqueue_frontend_scripts', 20 );

And that’s all the PHP we need! Because our solution changes the default matching behaviour we are careful to only enqueue this script on the Checkout page using WooCommerce’s is_checkout() function.

Client-Side

The enqueued Javascript file is pretty simple as well:

(function($) {
    // Based on custom matcher here: https://select2.org/searching#customizing-how-results-are-matched
    function start_matcher(params, data ) {
        
        // If there are no search terms, return all of the data
        if ($.trim(params.term) === '') {
          return data;
        }

        // Do not display the item if there is no 'text' property
        if (typeof data.text === 'undefined') {
          return null;
        }

        // `params.term` should be the term that is used for searching
        // `data.text` is the text that is displayed for the data object
        // Make sure to compare in the same case! 
        if ( data.text.toLowerCase().indexOf(params.term.toLowerCase() ) === 0 ) {
            return data;
        }
        // Return `null` if the term should not be displayed
        return null;
    }
    $.fn.select2.defaults.set('matcher', start_matcher );
})(jQuery);

There’s a closure so we don’t pollute the global namespace, then we define a new matcher function that matches the beginning of the phrase, and set it as the default matcher. This code runs as soon as it’s ready – it doesn’t wait for a DOMReady event, because by the time DOMReady fires the SelectWoo boxes have probably been initialized already.

Conclusion

I wish we didn’t have to set the defaults for the whole page, but that’s what we’re stuck with at the moment. We could apply different matching patterns to different pages by adding a few more conditions to the PHP block, and a bit more Javascript. This works to make the checkout page work as expected, though.

All the code is up on Github as a WordPress plugin. If it’s useful let me know!

An animated Gif showing WooCommerce's autosuggest matching the beginning of each option.
This makes more sense!