Create a Custom WooCommerce Product Loop (The Right Way)

- 73 comments

Development,Projects,WooCommerce,WordPress,WordPress Filters

If you ever needed to create a custom WooCommerce Product loop then you probably used get_posts() or even a WP_Query(). But did you know that using those could actually cause your loop to stop working unexpectedly or even break your site? If you didn’t know that then I’m guessing you also didn’t know that WooCommerce actually has a native function for doing this. It’s ok, I didn’t know any of that either until I wanted to create a custom WooCommerce Product loop complete with all its addons and paging without repeating any code.

Skip to the final snippet

On a recent WooCommerce project I was tasked with building a page to display only Featured Products. WooCommerce actually has a handy function to make getting Featured Product IDs very quick, wc_get_featured_product_ids(). Using that, it’s fairly trivial to plug that in to a custom WP_Query() and fire out a vanilla custom loop (or use get_posts() if you’re determined to do it the long way). I could even use the Featured Products shortcode, but those methods aren’t ideal. Unfortunately, anyone searching for the right way to accomplish this will find no shortage of stackexchange posts offering WP_Query() and get_posts() solutions. Let’s talk about why those aren’t great and what the best practices are.

The Downsides of using WP_Query & Shortcodes

The downside to a custom WooCommerce loop using any of the methods above is that I’d end up losing the WooCommerce goodies that are baked into WooCommerce’s native loop. I wouldn’t get a nifty ordering dropdown to sort products by various criteria, and I wouldn’t get my much needed pagination. I can add those elements back into the loop myself, but it’s a good amount of repetitive code I’d like to avoid adding to the project, especially because WooCommerce already comes with that code. In addition, if I’ve used any of the WooCommerce action or filter hooks to change the main WooCommerce loop behavior or appearance then I’ll need to account for those changes in my custom loop.

One more important downside is mentioned in the WooCommerce GitHub Wiki (emphasis added):

Building custom WP_Queries or database queries is likely to break your code in future versions of WooCommerce as data moves towards custom tables for better performance. This is the best-practices way for plugin and theme developers to retrieve multiple products.

After reading that and considering the other downsides of using WP_Query() and other methods to create a custom WooCommerce product loop, I decided to dig around and devise the most Woo-native way of creating a custom product loop to keep all the WooCommerce loop features while still leaving room to make the customizations I need. In my case, and the example I’ll be demonstrating, that means displaying only Featured Products on a page made to closely resemble the main Shop page.

A Better Way

My quest for a better WooCommerce custom loop started where all great copies start: the original. So I took a look at archive-product.php inside my WooCommerce installation and began analyzing it. It’s actually very simple, but it’s also very nuanced. So I’m going to follow the structure of the WooCommerce Archive Template, including its action hooks and filters (along with a few other important bits I’ll need to add), but instead of using the WordPress Archive loop (which isn’t available in a standard page template) I’ll use wc_get_products() to follow best practices mentioned in the Wiki above.

Here’s what we’re starting with:

This is the simple base we’re starting with. It’s a very basic WordPress loop with a few action hooks and WooCommerce functions. Let’s dig in to how we can replicate this with our custom loop.

Getting Started

The first thing I did was make a Page Template. If you found this post then I assume you already know how to do that or you can use that link to figure it out. Next, I’ll copy the entire loop from the WooCommerce Product Archive template. However, I need to make some adjustments because that loop relies on the WordPress main Post Type Archive query. That query won’t be available on a Page (hence the need for a custom query/loop).

The Custom WooCommerce Product Loop

I’ll be using wc_get_products() as recommended on the WooCommerce Wiki so I need to adjust my code because that function (and even the WC_Product_Query Class) doesn’t give me the methods we’re all used to when creating custom loops, namely have_posts(). Instead of relying on have_posts() and the_post(), I’ll create a foreach loop. My project’s main Shop Page makes use of the WooCommerce ordering dropdown and pagination, so I’ll need to get some of the relevant variables to ensure those functions work here, and I also need to set some properties in the WooCommerce loop variable ($GLOBAL['woocommerce_loop']). Here’s what I came up with:

It’s actually fairly straightforward. First I get and set the variables for paging and order display in lines 7-11, then I get my Products, then I hack the WooCommerce loop variable with some parameters it needs in lines 25-30, then I finally loop through them using a foreach loop instead of the usual while loop because I don’t have the Loop methods to rely on as mentioned above. Lines 36-38 use the same form and function to temporarily set the global Post object as WooCommerce uses in its shortcodes, but hit the comments if there’s a better way.

Conclusion

Creating a custom WooCommerce Product loop on a Page in WordPress is often mentioned in tutorials and almost always calls for using a new WP_Query() or get_posts(). Both of those methods are certainly viable, but ultimately they may not keep pace with WooCommerce updates and so the right way to create a custom product loop is to use wc_get_products() and a foreach loop or WC_Product_Query(). It’s also possible through a few quick tricks to persuade WooCommerce into outputting its goodies like ordering dropdowns and pagination to save you some time and save your project from repetitive code. This method can be used just about anywhere in your templates and does a thorough job of outputting a WooCommerce Product loop that matches up with the default Product loop on your Shop and Product Category pages. Any changes you’ve made to the WooCommerce loop’s action hooks or filters will be output when using this snippet so you won’t need to repeat yourself.

I hope this helps more WordPress and WooCommerce developers implement custom Product loops the right way. If you have questions or comments or noticed something I may have overlooked please leave a comment below!


Comments

  1. Hi Michael,
    Thanks for this tutorial, I just tested it with my woocommerce site (staging environment): made a copy of archive-product.php in my storefront child-theme template folder and added the code above. I want to filter the archive page based on a specific attribute “winkel”, so I added this to the $featured_products variable:

    'tax_query' => array(
    array(
    'taxonomy' => 'pa_winkel',
    'field' => 'name',
    'terms' => array('De Hooiberg'),
    'operator' => 'IN',
    )
    ),

    I see this partially works: It still shows all categories with the number of products of the unfiltered loop. Below that it shows the 2 products that indeed have “De Hooiberg” in the winkel-attribute. I’d like to make this fully function on the product archive page. So that it only shows the categories which contain products with this attribute value. Is this possible too?

  2. Hi Michael,
    Thank you very much for this tutorial. I’ve been able to get this php code more or less working but with two caveats…
    1) the array result never shows all articles, must of the time just 10% of all articles concerned.
    2) a meta_query with an ACF-field is not taken into account:
    ‘meta_query’ => array(
    ‘key’ => ‘release_date’,
    ‘value’ => $today,
    ‘compare’ => ‘>’,
    ),
    I do not understand why in both cases.
    Thanks in advance for having a look.

    1. You can see the Woo wiki has some details on custom parameter support @ https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query#adding-custom-parameter-support. Not sure if that will get you where you want. If that doesn’t work then you may want to run a WP_Query() first to obtain a list of product IDs based on the meta_query and then use that list of IDs in your wc_get_products() call by putting it in the include parameter. It would be two queries which could be resource intensive, but it would get you there most likely.

  3. all working fine however I get this error notice “Notice: Only variables should be passed by reference in E:\Programs\XAMPP\htdocs\mttwood_www\wp-content\themes\mttwood\woocommerce\archive-product.php on line 23”

    the code on line 23 is “$ordering[‘orderby’] = array_shift(explode(‘ ‘, $ordering[‘orderby’]));”

  4. Hi, is there any way to make a sorting for Trending products — products that are bestsellers for last 30 days? And woocommerce everyday checks for number of sales of all products and make a dynamic sorting among all shop products as Trending products?

  5. Hi Michael.
    Thank You for great tutorial. I’m using this method to build custom loops in my blocks, and it works great. But on my main shop page I’m using ajax to load more products. I’m using the WP_Query for this, but You are right – if there is a built in woocommerce method, we should use it. But how can I filter products bu price or by attributes? In WP_Query I’m using the meta_query and tax_query parameters? Is there a way to set this with wc_set_loop_prop function? Thanks

  6. The Custom WooCommerce Product Loop is great to display on a page BUT I want to add featured products in the woocommerce order email, how i will customize its html/css

  7. My biggest beef with this neat tutorial and code snippet is the amount of queries you’re ultimately making. The wc_get_products() function calls a query but you’re only returning IDs. At that point you loop through the ID and thing ping the database each time for get_post() When instead you should be returning post objects or Products ( as WooCommerce will also load the associated post and cache it ).

    In any case, it’s neat and thank you for putting it together.

    1. Hey Alex,

      sound very reasonable – how would the improved code look like? How did you tackle it in the end? I am eager to improve 🙂

      Thanks & regards!

  8. Hey,

    I’ve been struggling adding meta query to WP site.

    I’ve checked WP_Query, and the code works there, but as soon as I put this into WC_Product_Query, the meta_query doesn’t work anymore.


    $query_args = array(
    // 'page' => $custom_page,
    // 'limit' => $limit,
    'limit' => 50,
    'paginate' => true,
    'post_type' => 'product',
    // 'price' => 9.99,
    // 'orderby' => 'date',
    // 'order' => 'DESC',
    // 'orderby' => ['meta_value_num' => 'DESC', 'title' => 'ASC'],
    // 'orderby' => '_price',

            'meta_query' => array(
            'relation' => 'OR',
            array(
                'key'     => '_price',
                'value'   => array( 5, 10 ),
                'type'    => 'NUMERIC',
                'compare' => 'BETWEEN',
            ),       
        ),
        'tax_query' => array( 
            array(
                'taxonomy'      => 'pa_color',
                'field'         => 'slug',
                'terms'         => $custom_colors,//$custom_colors,
                'operator'      => $custom_operator
            ),
        ),
        'return'   => 'ids'
    );
    
    $query = new WC_Product_Query( $query_args );
    

    If this was wp_query, it would filter by price. I have no idea how to fix this. Searched the entire internet. WooCommmerce is up to date, but this just doesn’t work.

  9. What if the page is for infinite scroll and not for pagination? I’ll get duplicate products on the scroll.

  10. how can we do this “$featured_products->max_num_pages” when “wc_get_products” returns an array?

  11. I’m trying to get the ordering working with an AJAX call. According to the wiki/docs the orderby parameter does not accept the standard list of “values” like: “menu_order”. How do you get the native ordering to work?

    1. Howdy! That would be in the theme customizer in Appearance -> Customize -> WooCommerce -> Product Catalog -> Products per row & Rows per page. Hope that helps!

  12. Hi Michael,

    Thanks for writing such a detailed instruction. This has really helped me understand WooCommerce loops. I’m new to this but I’m fairly sure I get it all, but I’m hoping you can help me with one thing.

    “Out of the box’ I just get a blank

    <

    ul>. When I var_dump the array I can see it is populated with the ID’s, which confused me, as I would have expected the rest of the code to work. I wondered if removing the -> product from the foreach loop might work, and voila, all of the ‘s were output.

    I’m just wondering if that is an error above, or if you had another explanation as to why that might have made it work in my case? Not super important so no sweat, just thought it might help another user facing a similar situation, and I’m interested as to whether this points to a different error in my case.

    Thanks!!!

  13. This is exactly what I was after, but I’m having an issue with the Pagination. Pagination is displayed and works as expected, until loading the new page, which returns a 404. Any ideas on how I can solve this?!

  14. Great tutorial thanks. If I change the number of posts per page to 8, the count still says “showing all 10 products” rather than showing “showing products 1-8 of 10” – any idea how to fix this?

    1. Hi Catherine, where are you changing the number of posts per page?

      Unless you’re changing the number of products per page in the WooCommerce loop using the variables in my code snippet—either by directly setting $products_per_page or by using the loop_shop_per_page somewhere else in your theme, then you might not be changing that setting even though you think you are.

      The correct place to set that number of products per page in the WooCommerce loop is in Appearance -> Customize -> WooCommerce -> Product Catalog -> Products per row & Rows per page.

      I hope that helps!

  15. Hello and thank you for the post!
    The pagination while it is displayed, it doesn’t work.
    It redirects to page/2/, but it shows 404 error

    1. Apologies for the late reply George. Please visit Settings -> Permalinks and click “Save” to flush your site’s permalinks.

  16. Hi Michael,
    really great tutorial thank you sho much to share it!

    I’ve read the previous comments and I understand that you’re not able to debug any kind of WordPress page and If you have time to look at my code here’s a Gist

    I do not understand why but my templates shows only three product instead of the 8 I’ve limited the loop to. In the Gist I’ve added the content-product.php custom template file where I add some action hook to inject some code but that’s it for my concern about query customization.

    I think the problem relies on the setup_postdata() calls because if I vdump() (thanks to this plugin ) I can see that the actual WP_Post Object is created but somewhat the template file does not load it.

    How can I check the value of setup_postdata() in the template file?

    1. Happy to solve my own problem. The issue where in the if $product->is_visible() was hiding some hidden product I have in my store.

      Once I added 'visibility' => 'catalog', in the wc_get_products() call I’ve started to see my 8 visible products.

      Thank you again for this amazing article!

      1. Thanks for your comments Andrea. I’m sorry I didn’t reply until now but I’m glad you figured it out!

  17. Hi Want to create a Shortcode in WooCommerce which exclude featured products once shortcode exclude-featured=true/false. As well want to filter using the same shortcode in between publish date. If I added start-date=04/04/2019 end-date=05/04/2019 as a shortcode parameter then it will filter products in between these 2 dates.

    So please kindly guide me and provide me the solution of it,

    Thanks

  18. wc_set_loop_prop('total', $featured_products->total); produces an error: Trying to get property 'total' of non-object

    1. If you changed the name of the variable where you stored the results of wc_get_products() then you’ll need to change $featured_products on that line, too. It should be $your_variable_name->total.

  19. Thanks for this. But for some reason original query mixed with the new one(Im changing a tempate for one specific category and custom loop shows all subcatgories first and then displays custom query results) Plus pagintaion doesn’t work on page one. But when i add ‘page/2/’ to url – paginatin works.

  20. Hey Michael, great post thanks! it came up top on the search results.

    I am using latest woocommerce and storefront. When I select the ‘sort by average rating’ the code breaks, because what is returned is an array and not a string.

    print_r( $ordering ):
    <em>Array ( [orderby] => Array ( [meta_value_num] => DESC [ID] => ASC ) [order] => ASC [meta_key] => _wc_average_rating )</em>
    
    Warning: explode() expects parameter 2 to be string, array given in /home/pall/public_html/wp-content/themes/storefront-child/template-custom.php on line 46
    
    Warning: array_shift() expects parameter 1 to be array, null given in /home/pall/public_html/wp-content/themes/storefront-child/template-custom.php on line 46
    
  21. Hey Michael,

    Unfortunately, your code snippet crashed when I ran it verbatim, but after looking into what I could do with WP_Product_Query (thanks for pointing me to that Wiki btw!), here’s what I came up with:

    $args = array( 
        'status' => 'publish',
        'limit' => -1,
        'return' => 'ids',
    ); // Basically, get all product IDs.
    
    $wc_query = new WC_Product_Query($args); // Run the $arg through WC_Product_Query (just like WP_Query)
    
    $wc_products = $wc_query->get_products();
    

    Grab all the product data. If you print it out,
    you’ll see an ugly behemoth of protected data arrays.

    What to do about those?

    Use the get_data() method (courtesy of the WC_Data class) to grab whatever you need.

    Here’s how:

    Since we already told the WooCommerce API that we
    just wanted product IDs in the first set of arguments that we
    passed to the WC_Product_Query object, now we can run those product
    IDs through the wc_get_product() function and call the get_data() method
    on the resulting $product_data variable like so:

    $product_data_array = array();
    
    foreach ($wc_products as $product_id) {
    
      $product = wc_get_product($product_id);
    
      $product_data = $product->get_data(); // This will "unlock" the protected data arrays so you can grab the data you need.
    
      // Grab all the properties you want / need.
    
      $product_data_array[] = $product_data['id'];
      $product_data_array[] = $product_data['name'];
      $product_data_array[] = $product_data['price'];
      $product_data_array[] = $product_data['short_description'];
    }
    

    Finally, just so you don’t have to stare at an ugly block of PHP-garbled text,
    print your freshly minted product data array between a set of pre tags to see your results. The end! =)

    echo '
    
    ';
    
    print_r($product_data_array);
    
    echo '
    
    ';
    

    Good luck everyone! =)

  22. Thanks for this great content. But I do have one question about using AJAX with this method to handle my own custom page pagination and filters for the products. I would usually do something like creating a wp_query to get the post data store it in a variable then covert it into JSON and pass it to my AJAX function to output to the page. But how would I be able to pass all the product data to my ajax function using this method or would it be best to use wp_query in this situation? I can provide an example on how I would do this with wp_query if I wasn’t clear enough. Thanks

    1. The function and class in the post are basically wrappers for get_posts() and WP_Query, so you should be able to use the Woo function/class just fine. Have you tried it? Specifically what “product data” do you think you’ll need that you can’t fetch with these Woo functions?

  23. Thanks a lot for this post. Please help, how can I use this solution with orderby form on custom page(no category, taxonomy)?

  24. Great article, very very helpful.
    However, I’m trying to use this to only show products of certain categories. Would that be possible ? Do I have to change the $featured_products array to only call the category I need? Thanks for any help you might give me.

    1. Hi Antonin, glad you found this helpful. For a category, you can omit the featured argument and instead use the category argument, which accepts an array of categories by slug. So something like category => array('category-slug-1', 'another-category-slug') should work.

          1. Never came across the wiki before! Thanks for that!
            I wanted to ask another question, you might have a quick answer, if not no worries. But I’ve been trying to add a dropdown filter menu for attributes to appear in the woocommerce_before_shop_loop ( so that it sits next to the catalog ordering dropdown).
            By creating a custom widget region and using the native product attribute filter widget I was able to get it to work. The issue is that the widget only works on the archive and shop page, and I want it to work on any page. There’s a link that I can’t seem to find.
            Tried looking for ressources on how to create a custom attri filter dropdown from scratch but no luck there either.
            Just wondering if you have a quick tip to give me.
            Again thanks so much for your time and your help.

            1. Hi, I have the same problem. Did you find the solution how to show attributes filter on your custom page/custom product loop?

  25. I hope you can help me. I’m trying to get a shortcode to expand on the Shop page within the product short description text.

    The shortcode is added to the product short description text box. It expands as expected on the single product page and using the quick view but not on the Shop page.

    I think I need to edit the archive-product.php file in my child theme but I’m not sure how/what to edit to get it to work.

    Any help would be greatly appreciated.

  26. Hey Michael, excellent tutorial.

    Do you have any other article where explains how exactly does the WC loop works? I’m having a trouble with that.

    Thanks in advance.

    1. Hi Edinaldo, thank you for the kind words. The WC loop is just a basic WordPress loop which you can read more about here: https://codex.wordpress.org/The_Loop. WC adds a few action hooks and has some variables of its own that it uses to setup the loop, but apart from that it’s a standard WordPress loop. If you let me know what you’re stuck on I can try to shed some light where the Codex might be unclear.

      1. Thanks for replying. Alright, i’ll try to be clear as possible.

        In my home page i need to display only the featured products on the top and, below this all the products are displayed with a excerpt of it’s description limited to 50 chars and the button to add to the cart. Then, in another page i need to create a filter for all products categories, displaying only the products that matches with the filter.

        I’m using the Outlet child theme for Storefront.
        Thanks already

        1. What have you tried so far and what issues are you having? If you link to a Gist of your template files and let me know specifically where you’re having difficulty then I can try to help more.

  27. Hi, thank you for the tutorial, it was great, but I have a problem. I want to show products with custom fields ( like 5 of them ) in products archives or product category
    Is it possible?
    Like brand and color or everything important about my products
    Thank you for your time.

    1. Thanks Dave. Not sure what you mean, but the function is actually wp_reset_postdata(), so make sure you’re calling it correctly. In order to troubleshoot more, I’d need to see your code. Also, you can try commenting out any other custom functions in your functions.php that may be customizing/affecting your Page Template or the WooCommerce loop.

  28. Very good read! Thank you. However when I use above code some pages contain 11 products, some 12 and some 13. How could I force the number to be 12 at all times?

    1. Great tutorial. but as Robbert is saying, the first pages is showing 1 product less than the following pages. how can we solve this?

      1. Dave, glad it’s helpful, but as I’ve replied to everyone else who reported problems in the comments, I’d need to see the entire template you’re using this code in to be able to pinpoint any possible issues. Even then, there may be hooks in use in your functions.php, plugins, or elsewhere that are affecting the loop. Your best bet is to post the complete template code in a Gist, or debug some of the loop variables you’re using, including running print_r() on the custom query variable itself to ensure it’s returning the correct number of posts.

  29. Love the tutorial, really helped! I have a further query. I am using the above code in a site where I need to make a custom loop on several pages. So far I have this working thanks to your great tutorial. What I need to do now is filter one of them for products that are on sale. I was looking at another example of this that uses WP_Query and they use a meta query with an OR operator to test for sales price > 0 for both simple and variable products. Could you suggest how I would achieve this using the loop above?

    1. Ryan, thanks for the kind words, I’m glad this helped you. What you want to do should actually be quite easy. Fortunately, the function I alluded to in the intro—wc_get_featured_product_ids()—has a cousin called wc_get_product_ids_on_sale() (see here), so you’re in luck 🙂 For future reference, that’s a great file full of useful functions. So you can try something like this for your loop arguments:

        $sale_products           = wc_get_products(array(
          'meta_key'             => '_price',
          'status'               => 'publish',
          'limit'                => $products_per_page,
          'page'                 => $paged,
          'include'              => wc_get_product_ids_on_sale(),
          'paginate'             => true,
          'return'               => 'ids',
          'orderby'              => $ordering['orderby'],
          'order'                => $ordering['order'],
        ));
      

      Notice that all I did was remove the 'featured' => true and replaced it with 'include' => wc_get_product_ids_on_sale(). I hope that helps.

  30. I tried to follow his steps, but it does not pass the condition if ( wc_get_loop_prop( 'total' ) ). Can you help?

    1. Sure Lucas, can you please post your full template source code into a GitHub Gist or a StackExchange question? I’ll be happy to take a look.

  31. This is a great piece of code! I am having trouble with pagination though. I get a 404 error when moving past page 1.

    Any thoughts? Thanks!

    1. Thanks John. You can try flushing your permalinks (just visiting Settings -> Permalinks in your admin will do that). If that doesn’t work, post your entire template’s code here or in a Gist and I’ll have a look.

Leave a Comment