Implement Ajax-Based Recently Viewed Products in WooCommerce, Compatible with Cache Plugins

In the competitive world of e-commerce, providing a personalized and seamless shopping experience is crucial.

One effective way to achieve this is by displaying recently viewed products to customers. When users can easily revisit the products they have shown interest in, it enhances their overall browsing experience and encourages them to make informed purchasing decisions.

1. Storing viewed products data using cookies

add_action( 'template_redirect', function() {
    global $post;

    if ( ! is_singular( 'product' ) ) {
        return;
    }

    if ( isset( $_COOKIE['recently_viewed_products'] ) && !empty( $_COOKIE['recently_viewed_products'] ) ) {

        $viewed_products = (array) explode( '|', $_COOKIE['recently_viewed_products'] );
        
    } else {
        
        $viewed_products = array();
    }

    if ( ! in_array( $post->ID, $viewed_products ) ) {
        $viewed_products[] = $post->ID;
    }

    if ( count( $viewed_products ) > 10 ) {
        array_shift( $viewed_products );
    }

    wc_setcookie( 'recently_viewed_products', implode( '|', $viewed_products ) );

}, 20 );

The template_redirect hook is called every time a new page is accessed.

The first step in line 4 is to check if the current page is a single product page.

In line 8, I verify if a cookie called recently_viewed_products exists and is not empty. If it exists, I split its value into an array.

From lines 17 to 19, I add the current product ID to the array if it doesn't already exist.

Between lines 21 and 23, I ensure that no more than 10 products are saved.

Finally, in line 25, I save the products to the browser cookies using the WooCommerce function wc_setcookie().

2. Displaying the recently viewed products dynamically

<?php
add_action( 'woocommerce_after_single_product_summary', function(){

    ?>
        <section class="recently-viewed products">
            
        </section>

        <script>
            (function($){
                $( document ).ready(function() {

                    var value   = `; ${document.cookie}`;
                    var parts   = value.split('; recently_viewed_products=');

                    if ( parts.length === 2 ) {
                        var viewed_products = decodeURIComponent( parts.pop().split(';').shift() );
                    } else {
                        return;
                    }

                    if ( viewed_products ) {

                        $.ajax({
                            url: '<?php echo admin_url('admin-ajax.php'); ?>',
                            type: 'POST',
                            data: {
                                action: 'recently_viewed_products_action',
                                nonce: '<?php echo wp_create_nonce( "recently_viewed_products_nonce" ); ?>',
                                viewed_products: viewed_products
                            },
                            success: function (response) {
                                if ( response.success ) {
                                    $('.recently-viewed.products').html( response.data );
                                }
                            }
                        });
                    }

                });
            })(jQuery);
        </script>
    <?php

}, 21 );

To display the recently viewed products, you can utilize the flexibility of WooCommerce hooks. In this case, we are using the woocommerce_after_single_product_summary hook. However, you have the freedom to choose any hook that suits the specific position where you want the products to be displayed.

From lines 5 to 7, an HTML section is added to serve as a container.

In line 13, we retrieve the cookies using jQuery. The reason for starting with a semicolon is to ensure that the code executes properly, even if our cookie recently_viewed_products is at the beginning of document.cookie.

In line 14, the split() method is used to split a string into an array of substrings and return the new array.

In line 16, we check if the length is equal to 2, as that indicates that our cookie recently_viewed_products exists.

In line 17, decodeURIComponent() is used to decode the string, pop() retrieves the last element of the array, split() splits that last element by a semicolon, and shift() fetches the first element of the array generated by the split() method.

From lines 24 to 37, an AJAX call is created using jQuery.

3. recently_viewed_products_action WP Ajax Action

<?php
add_action( 'wp_ajax_nopriv_recently_viewed_products_action', 'recently_viewed_products_cb' );
add_action( 'wp_ajax_recently_viewed_products_action', 'recently_viewed_products_cb' );
function recently_viewed_products_cb() {

    $nonce = isset($_POST['nonce']) ? $_POST['nonce'] : '';
    if( !wp_verify_nonce( $nonce, 'recently_viewed_products_nonce' ) ) {
        wp_send_json_error( 'Invalid nonce' );
    }

    $viewed_products    = isset( $_POST['viewed_products'] ) || ! empty( $_POST['viewed_products'] ) ? (array) explode( '|', wp_unslash( $_POST['viewed_products'] ) ) : array();
    
    if ( empty( $viewed_products ) ) wp_send_json_error( _e( 'No products', 'text-domain' ) );
    
    $viewed_products    = array_reverse( array_filter( array_map( 'absint', $viewed_products ) ) );

    ob_start();
        ?>
            <div class="container">
                <h2><?php _e( 'Recently viewed', 'text-domain' ); ?></h2>

                <?php
                    woocommerce_product_loop_start();

                    foreach ( $viewed_products as $viewed_product_id ) {

                        $post_object = get_post( $viewed_product_id );
                        setup_postdata( $GLOBALS['post'] =& $post_object );

                        wc_get_template_part( 'content', 'product' );
                    }

                    woocommerce_product_loop_end();
                ?>

            </div>
        <?php

        wp_reset_postdata();

    $html = ob_get_clean();

    wp_send_json_success( $html );
}

The first step in an Ajax call is to verify the nonce. This verification process is implemented from lines 6 to 9.

In line 11, we retrieve the product IDs from the $_POST variable.

In line 13, we check if there are no products available. If so, an error message is returned.

In line 15, we perform a series of operations on the $viewed_products array. First, array_map('absint', $viewed_products) removes any non-numeric characters from the array. Then, array_filter() removes any empty values from the array, and finally, array_reverse() reverses the order of elements in the array.

Starting from line 17 to 41, we generate HTML markup to create a container with a heading. Additionally, we iterate over an array of viewed product IDs, fetching and displaying the corresponding product content using WooCommerce templates. The resulting HTML output is stored in the $html variable.

In line 43, we send the HTML content back to the browser for display.