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.
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:
<?php | |
if ( have_posts() ) { | |
do_action( 'woocommerce_before_shop_loop' ); | |
woocommerce_product_loop_start(); | |
if ( wc_get_loop_prop( 'total' ) ) { | |
while ( have_posts() ) { | |
the_post(); | |
do_action( 'woocommerce_shop_loop' ); | |
wc_get_template_part( 'content', 'product' ); | |
} | |
} | |
woocommerce_product_loop_end(); | |
do_action( 'woocommerce_after_shop_loop' ); | |
} else { | |
do_action( 'woocommerce_no_products_found' ); | |
} |
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:
<?php | |
if(!function_exists('wc_get_products')) { | |
return; | |
} | |
$paged = (get_query_var('paged')) ? absint(get_query_var('paged')) : 1; | |
$ordering = WC()->query->get_catalog_ordering_args(); | |
$ordering['orderby'] = array_shift(explode(' ', $ordering['orderby'])); | |
$ordering['orderby'] = stristr($ordering['orderby'], 'price') ? 'meta_value_num' : $ordering['orderby']; | |
$products_per_page = apply_filters('loop_shop_per_page', wc_get_default_products_per_row() * wc_get_default_product_rows_per_page()); | |
$featured_products = wc_get_products(array( | |
'meta_key' => '_price', | |
'status' => 'publish', | |
'limit' => $products_per_page, | |
'page' => $paged, | |
'featured' => true, | |
'paginate' => true, | |
'return' => 'ids', | |
'orderby' => $ordering['orderby'], | |
'order' => $ordering['order'], | |
)); | |
wc_set_loop_prop('current_page', $paged); | |
wc_set_loop_prop('is_paginated', wc_string_to_bool(true)); | |
wc_set_loop_prop('page_template', get_page_template_slug()); | |
wc_set_loop_prop('per_page', $products_per_page); | |
wc_set_loop_prop('total', $featured_products->total); | |
wc_set_loop_prop('total_pages', $featured_products->max_num_pages); | |
if($featured_products) { | |
do_action('woocommerce_before_shop_loop'); | |
woocommerce_product_loop_start(); | |
foreach($featured_products->products as $featured_product) { | |
$post_object = get_post($featured_product); | |
setup_postdata($GLOBALS['post'] =& $post_object); | |
wc_get_template_part('content', 'product'); | |
} | |
wp_reset_postdata(); | |
woocommerce_product_loop_end(); | |
do_action('woocommerce_after_shop_loop'); | |
} else { | |
do_action('woocommerce_no_products_found'); | |
} |
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!
The global $product is not being set. Iām running this code on my home page and it doesnāt output any products.
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?
Fatal error: Uncaught Error: Call to undefined function wc_get_default_products_per_row()
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.
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 themeta_query
and then use that list of IDs in yourwc_get_products()
call by putting it in theinclude
parameter. It would be two queries which could be resource intensive, but it would get you there most likely.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ā]));ā
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?
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
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
Great tutorial, but it donāt seem to work with the filtering widgets.
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 forget_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.
Thanks & regards,
Henning
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!
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',
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.
What if the page is for infinite scroll and not for pagination? Iāll get duplicate products on the scroll.
Hello
Did you managed to solve this?
how can we do this ā$featured_products->max_num_pagesā when āwc_get_productsā returns an array?
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?
This was a HUGE help. Thank-you Michael!
Great tutorial, thank you!
How do I change the number of products per row?
Howdy! That would be in the theme customizer in Appearance -> Customize -> WooCommerce -> Product Catalog -> Products per row & Rows per page. Hope that helps!
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!!!
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?!
Hi Dan, try visiting Settings -> Permalinks and just click āSaveā to flush your permalinks.
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?
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 theloop_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!
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
Apologies for the late reply George. Please visit Settings -> Permalinks and click āSaveā to flush your siteās permalinks.
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 Ivdump()
(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?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 thewc_get_products()
call Iāve started to see my 8 visible products.Thank you again for this amazing article!
Thanks for your comments Andrea. Iām sorry I didnāt reply until now but Iām glad you figured it out!
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
wc_set_loop_prop('total', $featured_products->total);
produces an error:Trying to get property 'total' of non-object
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
.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.
How do I exclude a category of products?
Thanks for the great post. How would I exclude one category of products from the loop?
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.
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: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 productIDs through the
wc_get_product()
function and call theget_data()
methodon the resulting
$product_data
variable like so: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! =)
Good luck everyone! =)
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
The function and class in the post are basically wrappers for
get_posts()
andWP_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?Thanks a lot for this post. Please help, how can I use this solution with orderby form on custom page(no category, taxonomy)?
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.
Hi Antonin, glad you found this helpful. For a category, you can omit the
featured
argument and instead use thecategory
argument, which accepts an array of categories by slug. So something likecategory => array('category-slug-1', 'another-category-slug')
should work.Ah man so simple, didnāt think of just changing that. Thanks so much for the quick response!
Sure thing, happy it worked! When in doubt check the WooCommerce wiki for other valid arguments: https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query
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.
Hi, I have the same problem. Did you find the solution how to show attributes filter on your custom page/custom product loop?
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.
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.
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.
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
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.
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.
Hi Haamed, you can easily filter the loop by meta keys by checking this section of the WooCommerce Wiki: https://github.com/woocommerce/woocommerce/wiki/wc_get_products-and-WC_Product_Query#adding-custom-parameter-support
Thank you, this was very helpful to me. The code is very clean and efficient. Much appreciated!
Wp_reset_post_data
is not working. Still returns āoldā data. BTW nice tut.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 yourfunctions.php
that may be customizing/affecting your Page Template or the WooCommerce loop.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?
Great tutorial. but as Robbert is saying, the first pages is showing 1 product less than the following pages. how can we solve this?
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 runningprint_r()
on the custom query variable itself to ensure itās returning the correct number of posts.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?
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ā
For future reference, thatās a great file full of useful functions. So you can try something like this for your loop arguments:
wc_get_featured_product_ids()
āhas a cousin calledwc_get_product_ids_on_sale()
(see here), so youāre in luckNotice that all I did was remove the
'featured' => true
and replaced it with'include' => wc_get_product_ids_on_sale()
. I hope that helps.Hey Michael,
Thatās fantastic! Iāll give that a go later and get back to you. Thanks for your help!
Sure thing, go slay some custom Woo loops!
That worked perfectly man! Youāre a legend
Thanks Ryan, glad to help!
I tried to follow his steps, but it does not pass the condition
if ( wc_get_loop_prop( 'total' ) )
. Can you help?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.
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!
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.
Very helpful tutorial.
Este post es ORO puro. Gracias por compartir. Gracias!
how can we use custom loop in Ajax call?
Nazrul, your question is pretty broad. How are you setting up your page, your AJAX call, and the loop? Please share as much code as possible using https://gist.github.com.