Create a Custom WooCommerce Product Loop (The Right Way)


Development,Projects,WooCommerce,WordPress,WordPress Filters

I suspect that if you’ve ever had to create a custom WooCommerce Product loop that you probably used get_posts() or even a WP_Query(). Did you know that could actually stop working unexpectedly? 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.


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!


    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.

  1. 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.

  2. 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.

    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.

  3. 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