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:
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 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.
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:
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.
Create or update something like the old WooCommerce migration plugin.
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.
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.
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.
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!
This week a client asked to add estimated ship times to the single-product pages in WooCommerce. Ship times are sometimes different for different products, so adding it as a custom field to each product made sense. As a bonus, there’s a “Shipping” tab in the Product Data metabox that the field can go in. Some Googling led me to Tom McFarlin‘s “Adding Custom Fields to Simple Products with WooCommerce” tutorial on tuts+, which is pretty thorough, but out of date – the actions he’s are no longer in WooCommerce, so it’s time for a new version of that tutorial, my-style. There are three main tasks to accomplish: Add the field to the WordPress back-end, save whatever is in the field, and output the data on the Single Product page. We’ll look at each task separately. The completed plugin is up on GitHub if you want to follow along.
Add the Custom Field to WooCommerce in the WordPress Back-End
WooCommerce provides an incredible number of hooks and filters. We’re going to us woocommerce_product_options_shipping hook, because it runs in the shipping block, but is an equivalent hook for each Product Data tab. Look at the files in the woocommerce/includes/admin/meta-boxes/views directory to understand how the Product Data, (and all the WooCommerce Metaboxes), are created, and what hooks are available.
Now we know what hook we need, it’s time for some code:
function print_admin_field() {
/**
* @var \WC_Product
* @see https://docs.woocommerce.com/wc-apidocs/source-class-WC_Meta_Box_Product_Data.html#42
*/
global $product_object;
if( $product_object->get_type() == 'simple' ) {
$field = [
'id' => 'sif_ships_in',
'label' => __('Ships In', 'jb-sif'),
'placeholder' => __('example: 1-2 days', 'jb-sif'),
'value' => get_post_meta( $product_object->get_id(), '_sif_ships_in', true ), // Pre-fill any values that have already been saved.
'description' => __( 'Add a "Ships in X" message below the Product Meta on the single-product page.', 'jb-sif'),
'desc_tip' => true
];
\woocommerce_wp_text_input( $field );
}
}
add_action( 'woocommerce_product_options_shipping', 'JB\SIF\print_admin_field');
We make sure we have the $product_object variable available, (it’s set in the WC_Meta_Box_Product_Data class, linked with the @see directive), then we make sure we’re adding the field to a “Simple” product type. Then, instead of writing a bunch of HTML, we set some info in an array, and pass it to the woocommerce_wp_text_input() function, (with a backslash, because the plugin is namespaced). WooCommerce has several of these helper fields available that create a form fields and their associated markup so they fit nicely with the rest of the WooCommerce admin.
Notice we’re already getting a value with get_post_meta(). That’s because the same code gets used every time the field is displayed – so if there’s a value in the database we want to display it. This is all we need to properly display a field in the Shipping section of the Product Data. We could add a name attribute to the $field variable, but it defaults to the value of id, so I left it out. Setting desc_tip to true puts the description in the tooltip that appears when someone hovers the circle with the question mark beside the field.
The last line of the code block above hooks our function into the woocommerce_product_options_shipping action. Because the plugin has its own namespace we need to include the full name of the function in the add_action() call.
Save Whatever is Entered in the Field
Again, a game of hooks figuring out how to save the data. Tom’s tutorial from last year used a woocommerce_process_product_meta hook which no longer exists. It looks like it’s been replaced by a woocommerce_process_product_meta_* hook for each product type. Since we’re only using a Simple product, we’ll use the woocommerce_process_product_meta_simple hook. Now we know which hook to use, some code:
In the save() function, (which we can name “save” without collisions because we’re using namespaces), we check the field is part of the $_POST array, check the nonce set by WooCommerce, and save the contents of the field, without forgetting to sanitize the data first.
WooCommerce may already checks the nonce for us, but I am not 100% sure, so I added an extra check.
Output the Data on the Single Product Page
WooCommerce’s plethora of hooks make placing the output relatively simple. I chose to put it right below the “Add to Cart” button on the Single Product page, but if you look through WooCommerce’s templates/content-single-product.php file and the files in the templates/single-product directory you’ll find lots hooks to use – and of course you’re not restricted to the single-product page. If you want to add your output to each product in a list of products take a look in the templates/content-product.php file.
function print_output() {
global $product;
$ships_in = get_post_meta( $product->get_id(), '_sif_ships_in', true );
if( !empty( $ships_in )): ?>
Our print_output() function is pretty simple: Grab the post_meta, check it’s not empty, and output it mixed in with a “Ships In” string. It’s hooked into the woocommerce_single_product_summary action with a priority of 45, which means it appear after the Add to Cart button and after the Product Meta. WooCommerce’s template files are really good at explaining what functions are hooked to actions, and the priority they’re hooked at, which makes it easy to place things on a page without having to copy templates over to your theme.
Conclusion
Adding fields to the WooCommerce part of a WordPress admin area is much easier than adding a whole metabox. You can grab the completed plugin on Github. You’ll see there’s a bit more to the plugin: requirements are checked using Mark Jaquith’s method before embarking on a namespaced plugin with shorthand array syntax.
If this is useful let me know, and feel free to open tickets and send pull requests on Github.