Archives: Snippets | Page 15

Basic WooCommerce Products Loop

This is a simple example of a basic WooCommerce products loop. If you want to learn more, here is a post about the right way to retrieve WooCommerce products in your theme.

<ul class="products">
	<?php
		$args = array(
			'post_type' => 'product',
			'posts_per_page' => 12
			);
		$loop = new WP_Query( $args );
		if ( $loop->have_posts() ) {
			while ( $loop->have_posts() ) : $loop->the_post();
				wc_get_template_part( 'content', 'product' );
			endwhile;
		} else {
			echo __( 'No products found' );
		}
		wp_reset_postdata();
	?>
</ul><!--/.products-->

Customize WooCommerce Breadcrumbs

You customize the WooCommerce breadcrumb with two filters: woocommerce_breadcrumb_defaults controls the markup around the trail, and woocommerce_get_breadcrumb controls the crumbs themselves. Below is a reference for both, checked against the WooCommerce source code.

If all you need is a different separator, the shorter guide Change the Breadcrumb Delimiter in WooCommerce covers that case. Add every snippet below to your child theme’s functions.php file.

Every woocommerce_breadcrumb_defaults Argument

The filter receives an array of six keys. These are the default values in woocommerce_breadcrumb() in includes/wc-template-functions.php:

Argument Default What it does
delimiter &nbsp;&#47;&nbsp; Printed between crumbs, never after the last one.
wrap_before <nav class="woocommerce-breadcrumb" aria-label="Breadcrumb"> Opening markup before the whole trail.
wrap_after </nav> Closing markup after the whole trail.
before Empty string Printed before each crumb.
after Empty string Printed after each crumb, before the delimiter.
home _x( 'Home', 'breadcrumb', 'woocommerce' ) Label of the first crumb. An empty value removes the Home crumb.

The aria-label on wrap_before was added in WooCommerce 8.3. Older tutorials show <nav class="woocommerce-breadcrumb"> without it.

Change Several Breadcrumb Defaults

This example turns the trail into an ordered list. Edit the keys you need and leave the others alone:

/**
 * Output the WooCommerce breadcrumb as an ordered list.
 */
function savvy_wc_breadcrumb_defaults( $defaults ) {
    $defaults['wrap_before'] = '<nav class="woocommerce-breadcrumb" aria-label="Breadcrumb"><ol>';
    $defaults['wrap_after']  = '</ol></nav>';
    $defaults['before']      = '<li>';
    $defaults['after']       = '</li>';
    $defaults['delimiter']   = '';
    $defaults['home']        = __( 'Store', 'your-textdomain' );
    return $defaults;
}
add_filter( 'woocommerce_breadcrumb_defaults', 'savvy_wc_breadcrumb_defaults', 20 );

With an empty delimiter you can draw separators in CSS, for example with .woocommerce-breadcrumb li + li::before.

How It Works

WooCommerce hooks woocommerce_breadcrumb() to woocommerce_before_main_content at priority 20. The function runs in this order:

  1. Applies woocommerce_breadcrumb_defaults to the default array.
  2. Merges any $args passed directly to woocommerce_breadcrumb( $args ), which override the filtered defaults.
  3. Adds the Home crumb (URL from woocommerce_breadcrumb_home_url) and builds the trail with the WC_Breadcrumb class.
  4. Fires the woocommerce_breadcrumb action, which generates BreadcrumbList schema.
  5. Loads global/breadcrumb.php.

The template echoes wrap_before, wrap_after, before, after and delimiter as raw HTML. Crumb names are escaped, and home passes through wp_strip_all_tags(), so HTML in the Home label is removed.

More Breadcrumb Customizations

These cover the other common requests: the Home link, the crumbs array and removing the trail.

Change the Home Link URL

By default the Home crumb points to home_url(). This sends it to the shop page instead:

function savvy_wc_breadcrumb_home_url( $url ) {
    return wc_get_page_permalink( 'shop' );
}
add_filter( 'woocommerce_breadcrumb_home_url', 'savvy_wc_breadcrumb_home_url' );

Modify the Crumbs with woocommerce_get_breadcrumb

The $crumbs array holds one entry per crumb, each an indexed array of name and URL: array( 'Home', 'https://example.com' ). This removes the product title from single product pages:

function savvy_wc_breadcrumb_crumbs( $crumbs, $breadcrumb ) {
    if ( is_product() && count( $crumbs ) > 1 ) {
        array_pop( $crumbs );
    }
    return $crumbs;
}
add_filter( 'woocommerce_get_breadcrumb', 'savvy_wc_breadcrumb_crumbs', 10, 2 );

The same array feeds the schema, so your changes appear there too. If you unset() a crumb from the middle, wrap the result in array_values() to keep the positions sequential.

Remove the Breadcrumb Completely

This is useful when you add breadcrumbs with an SEO plugin instead:

function savvy_remove_wc_breadcrumbs() {
    remove_action( 'woocommerce_before_main_content', 'woocommerce_breadcrumb', 20 );
}
add_action( 'init', 'savvy_remove_wc_breadcrumbs' );

Storefront moves the breadcrumb to storefront_before_content at priority 10, so on Storefront remove it from that hook instead. To hide it only on certain pages, see Remove WooCommerce Breadcrumbs on Specific Pages.

Storefront and Block Themes

Storefront filters woocommerce_breadcrumb_defaults at priority 10 to set its own delimiter, wrap_before and wrap_after. The parent theme loads after your child theme, so use priority 20 or higher to win.

In block themes the Store Breadcrumbs block calls woocommerce_breadcrumb(), so both filters on this page work there as well.

FAQs

Common questions about customizing WooCommerce breadcrumbs:

What is the difference between before/after and wrap_before/wrap_after?
wrap_before and wrap_after are printed once, around the whole trail. before and after are printed around every single crumb, which is how you output list items.
How do I remove the Home crumb?
Set $defaults['home'] to an empty string in the woocommerce_breadcrumb_defaults filter. WooCommerce only adds the Home crumb when that value is not empty.
Is there a WC()->breadcrumb property?
No. To get the crumbs in your own code, create new WC_Breadcrumb() and call its generate() method. That array has no Home crumb, since woocommerce_breadcrumb() adds it before generating.
Can I override the breadcrumb template instead?
Yes. Copy templates/global/breadcrumb.php to yourtheme/woocommerce/global/breadcrumb.php. Use the filters when you can, since overridden templates need updating when WooCommerce changes them.

Remove Product Content by Category in WooCommerce

The following code for example removes product images on the WooCommerce single product page only for products in the “Cookware” category. You can of course remove any content you want by using the WooCommerce hooks available to you.

/**
 * Remove product content based on category
 */
function remove_product_content() {
	// If a product in the 'Cookware' category is being viewed...
	if ( is_product() && has_term( 'Cookware', 'product_cat' ) ) {
		//... Remove the images
		remove_action( 'woocommerce_before_single_product_summary', 'woocommerce_show_product_images', 20 );
		// For a full list of what can be removed please see woocommerce-hooks.php
	}
}
add_action( 'wp', 'remove_product_content' );

Add Random Sorting to WooCommerce Catalog

The woocommerce_catalog_ordering() function prints the “Default sorting” dropdown above the product loop. To add your own option to it, such as random sorting, you need two filters: woocommerce_catalog_orderby for the label and woocommerce_get_catalog_ordering_args for the query.

The example below adds a Random option. The same pattern works for any orderby value that WP_Query supports.

Add a Random Sorting Option

Add this to your child theme’s functions.php file or to a custom plugin:

/**
 * Add a "Random" option to the catalog sorting dropdown
 * and to the default sorting choices in the Customizer.
 */
function savvy_catalog_orderby_random( $options ) {
    $options['random_list'] = __( 'Random', 'your-textdomain' );
    return $options;
}
add_filter( 'woocommerce_catalog_orderby', 'savvy_catalog_orderby_random' );
add_filter( 'woocommerce_default_catalog_orderby_options', 'savvy_catalog_orderby_random' );

/**
 * Turn the "random_list" value into WP_Query arguments.
 */
function savvy_catalog_ordering_args_random( $args, $orderby, $order ) {
    if ( 'random_list' === $orderby ) {
        $args['orderby'] = 'rand';
    }
    return $args;
}
add_filter( 'woocommerce_get_catalog_ordering_args', 'savvy_catalog_ordering_args_random', 10, 3 );

Visit the shop page and pick “Random” from the dropdown. The URL gets ?orderby=random_list and the products come back shuffled.

How It Works

woocommerce_catalog_orderby filters the options array that woocommerce_catalog_ordering() renders. The key becomes the orderby value in the URL and the value is the label.

woocommerce_default_catalog_orderby_options filters the “Default product sorting” choices in Appearance > Customize > WooCommerce > Product Catalog. The Customizer validates against this list, so without it you cannot save Random as the default.

When the shop query runs, WC_Query::get_catalog_ordering_args() reads the value from the URL or the default setting. It splits the value on a hyphen (price-desc becomes price and DESC), then passes the result to woocommerce_get_catalog_ordering_args as $args, $orderby and $order.

Two details matter:

  • Use underscores in custom keys. A key like random-list would be split into random and LIST.
  • Compare against $orderby, not $_GET. The function is also called by the [products] shortcode and blocks with their own values, and the parameter reflects what is actually being sorted.

The woocommerce_catalog_ordering() Function

WooCommerce hooks woocommerce_catalog_ordering() to woocommerce_before_shop_loop at priority 30, right after the result count at 20. It loads the loop/orderby.php template, and only outputs when the loop is paginated and products will be shown.

These are the default options, before filters:

Key Label
menu_order Default sorting
popularity Sort by popularity
rating Sort by average rating
date Sort by latest
price Sort by price: low to high
price-desc Sort by price: high to low

The function then adjusts the list. It removes menu_order when the default sorting is set to something else, removes rating when reviews or star ratings are disabled, and on search results replaces menu_order with relevance.

There is no dedicated sorting shortcode. The dropdown appears in [products paginate="true"], and block themes render it with the Catalog Sorting block, which calls the same function.

For querying products in your own templates, see Properly Retrieve WooCommerce Products in Your Template.

Random Sorting and Pagination

ORDER BY RAND() reshuffles on every request. On page 2 some products from page 1 can appear again while others never show up.

WP_Query accepts a seeded RAND(x) since WordPress 4.5. A seed that changes once a day keeps the order stable across pages and page caches:

if ( 'random_list' === $orderby ) {
    $args['orderby'] = 'RAND(' . absint( gmdate( 'Ymd' ) ) . ')';
}

Random ordering also forces MySQL to sort the whole result set, which gets slow on catalogs with many thousands of products.

FAQs

Common questions about WooCommerce catalog ordering:

How do I remove the sorting dropdown in WooCommerce?
In classic themes, add remove_action( 'woocommerce_before_shop_loop', 'woocommerce_catalog_ordering', 30 ); to functions.php. In block themes, remove the Catalog Sorting block from the product catalog template.
How do I remove a default sorting option?
Filter woocommerce_catalog_orderby and unset() the key, for example unset( $options['popularity'] );, then return the array.
Why is my custom sorting option not showing?
The dropdown only renders on paginated product loops with products to display. Also check that your theme or a page builder is not printing its own sorting control instead of woocommerce_catalog_ordering().
Can I sort by a custom field?
Yes. Add the option with woocommerce_catalog_orderby, then in woocommerce_get_catalog_ordering_args set $args['meta_key'] to your field and $args['orderby'] to meta_value or meta_value_num. Products without that field drop out of the results.

Allow HTML in Category and Tag Descriptions

By default, WordPress strips most HTML from category and tag descriptions when you save them. Only a short list of inline tags survives, so headings, lists, images and paragraphs with classes disappear the moment you click Update.

That gets in the way when you use category pages as landing pages with real intro content, which is one of the easier ways to optimize category pages for SEO. The snippet below allows HTML in category, tag and custom taxonomy descriptions.

The Code

Add this code to your child theme’s functions.php file or to a custom plugin:

/**
 * Allow HTML in term (category, tag) descriptions
 */
foreach ( array( 'pre_term_description' ) as $filter ) {
	remove_filter( $filter, 'wp_filter_kses' );
	if ( ! current_user_can( 'unfiltered_html' ) ) {
		add_filter( $filter, 'wp_filter_post_kses' );
	}
}

foreach ( array( 'term_description' ) as $filter ) {
	remove_filter( $filter, 'wp_kses_data' );
}

How It Works

WordPress sanitizes term descriptions with two filters, and the snippet removes both. Learn more about adding and removing filters in WordPress Hooks Explained.

  • pre_term_description runs before the description is saved. Core attaches wp_filter_kses here, which strips everything except a few basic tags.
  • term_description runs when the description is displayed. In the admin only, core adds wp_kses_data, which would strip the HTML again in the terms list table.

The current_user_can( 'unfiltered_html' ) check keeps it safe. Users with that capability can save any HTML, just like in the post editor.

Everyone else who can edit terms gets wp_filter_post_kses, the allowlist WordPress uses for post content. Tags like <h2>, <ul> and <img> work but <script> doesn’t.

Don’t remove wp_filter_kses without adding the fallback. That would let any user who can edit terms save scripts that run for every visitor.

What Changes After You Add It

The filters apply to every taxonomy, so the snippet covers categories, tags, custom taxonomies and WooCommerce product categories.

Situation Allowed HTML
Default, all users a, abbr, acronym, b, blockquote, cite, code, del, em, i, q, s, strike, strong
With the snippet, users with unfiltered_html Any HTML
With the snippet, other users The post content allowlist

Keep three things in mind:

  • Old descriptions aren’t restored. HTML was stripped before it reached the database, so re-enter it after adding the code.
  • On multisite, only super admins have unfiltered_html. Site admins get the post allowlist. The same applies to everyone if DISALLOW_UNFILTERED_HTML is set in wp-config.php.
  • Your theme has to print the description. term_description(), the_archive_description() and the Term Description block all output the stored HTML. A theme that escapes the description will show the tags as text.

If you’re building your own taxonomies, see how to create and query custom taxonomies. The wp_filter_kses() reference documents the sanitizer this snippet replaces.

FAQs

Common questions about HTML in category and tag descriptions:

Why does WordPress remove HTML from category descriptions?
Core runs wp_filter_kses on pre_term_description for every user, including administrators. It's a security default, and it only keeps basic inline tags such as links, bold and italics.
Does this work for WooCommerce product categories?
Yes. pre_term_description and term_description run for every taxonomy, including product_cat and product_tag. On product archives WooCommerce prints the description through wp_kses_post(), so scripts are removed there even if an admin saved them.
Does this add a visual editor to the description field?
No. The field stays a plain textarea, and you type the HTML yourself. The snippet only stops WordPress from stripping it.

Auto Display First Post Image or Default Fallback Image

Say you want to use the featured image (post thumbnail) that WordPress provides, but you have an archive with many posts and it would take a long time to add that featured image to each of them.

For new posts you can use the featured image in the standard way, but for old posts you want to use the first image in the post content as the featured image, or a default image if there are no images in that specific post. Add the following code to functions.php:

function catch_that_image() {
  global $post, $posts;
  $first_img = '';
  ob_start();
  ob_end_clean();
  $output = preg_match_all('/<img.+?src=['"]([^'"]+)['"].*?>/i', $post->post_content, $matches);
  $first_img = $matches[1][0];

  if(empty($first_img)) {
    $first_img = "/path/to/default.png";
  }
  return $first_img;
}

And to use this inside the WordPress loop:

if ( get_the_post_thumbnail($post_id) != '' ) {

  echo '<a href="'; the_permalink(); echo '" class="thumbnail-wrapper">';
   the_post_thumbnail();
  echo '</a>';

} else {

 echo '<a href="'; the_permalink(); echo '" class="thumbnail-wrapper">';
 echo '<img src="';
 echo catch_that_image();
 echo '" alt="" />';
 echo '</a>';

}

For more on WordPress image handling, see Image Sizes in WordPress.

Add Category Name to body_class in WordPress

The body_class function helps add classes to the body tag, which usually contains information about what type of page is currently displayed, and allows you to style specific elements on that page by that specific class.

For some reason WordPress does not add a class describing which category (or categories) you are in on a single post page (e.g. single.php). To add the category name to that body tag, use the following code:

function sv_add_category_to_single($classes) {
    if (is_single() ) {
        global $post;
        foreach((get_the_category($post->ID)) as $category) {
            // add category slug to the $classes array
            $classes[] = $category->category_nicename;
        }
    }
    // return the $classes array
    return $classes;
}
add_filter('body_class','sv_add_category_to_single');

This uses the body_class filter. Learn more in WordPress Hooks Explained.

Get the Featured Image URL in WordPress

To get the featured image URL in WordPress, use get_the_post_thumbnail_url(). It returns just the image URL instead of a full <img> tag, which is what you need for a CSS background-image, an Open Graph tag or a JavaScript slider.

get_the_post_thumbnail() returns the complete image element, and it is the right choice when you want an <img> with srcset. For the URL alone, use the snippets below.

Get the Featured Image URL

Add this inside the WordPress loop, in a template such as single.php or a template part:

<?php
if ( has_post_thumbnail() ) {
    $thumb_url = get_the_post_thumbnail_url( get_the_ID(), 'large' );
    ?>
    <div class="post-hero" style="background-image: url('<?php echo esc_url( $thumb_url ); ?>');"></div>
    <?php
}
?>

The variable $thumb_url holds the URL of the current post’s featured image in the large size.

How It Works

get_the_post_thumbnail_url( $post, $size ) has been in core since WordPress 4.4 (see the function reference). Both parameters are optional:

  • $post: a post ID or WP_Post object. Defaults to the global post in the loop.
  • $size: any registered size name, such as thumbnail, medium, large or full, or an array of width and height. Defaults to post-thumbnail.

The function returns false when the post has no featured image, so check has_post_thumbnail() first.

If the size name is not registered, WordPress returns the original image URL. See How to Define New Image Sizes in WordPress for adding your own sizes.

To print the URL directly, the_post_thumbnail_url( 'large' ) echoes it already escaped. The Featured Image panel only appears in the editor when your theme declares add_theme_support( 'post-thumbnails' ) in functions.php.

Outside the Loop or by Post ID

Pass the post ID as the first argument when you are outside the loop, for example in a widget or a hook:

$thumb_url = get_the_post_thumbnail_url( 42, 'full' );

When you also need the image dimensions, get the attachment ID first and use wp_get_attachment_image_src():

$thumb_id = get_post_thumbnail_id( 42 );
$image    = wp_get_attachment_image_src( $thumb_id, 'large' );

if ( $image ) {
    list( $url, $width, $height ) = $image;
}

If you only need the URL from an attachment ID, wp_get_attachment_image_url( $thumb_id, 'large' ) is the shorter version.

Featured Image URL from the REST API

A post object from the REST API only includes featured_media, which is the attachment ID. Add _embed=wp:featuredmedia to get the image data in the same request:

fetch( '/wp-json/wp/v2/posts?_embed=wp:featuredmedia' )
    .then( ( response ) => response.json() )
    .then( ( posts ) => {
        posts.forEach( ( post ) => {
            const media = post._embedded?.[ 'wp:featuredmedia' ]?.[ 0 ];
            const url   = media?.source_url;                        // original image
            const large = media?.media_details?.sizes?.large?.source_url; // large size, if generated
            console.log( post.id, url, large );
        } );
    } );

Limiting _embed to one relation works since WordPress 5.4, as described in the REST API global parameters docs.

If you also use _fields, include _links and _embedded in the list, or the embed is skipped. For a full example, see Show Posts from another Blog using REST API.

FAQs

Common questions about getting the featured image URL in WordPress:

How do I get the full-size featured image URL?
Pass 'full' as the size: get_the_post_thumbnail_url( get_the_ID(), 'full' ). This returns the URL of the uploaded image instead of a resized copy.
Why does get_the_post_thumbnail_url return false?
The post has no featured image set, or the function is looking at a different post than you expect. Outside the loop the global post may not be the one you want, so pass the post ID explicitly.
What is the difference between get_the_post_thumbnail and get_the_post_thumbnail_url?
get_the_post_thumbnail() returns a complete <img> tag with srcset, alt and dimensions. get_the_post_thumbnail_url() returns only the URL string.
Where is source_url in the REST API response?
With _embed, it is at _embedded['wp:featuredmedia'][0].source_url. Resized versions are under media_details.sizes, each with its own source_url.

Add Class to next_posts_link and previous_posts_link

These functions, if you are not familiar, create links to the previous and next post. They have many options but not one that lets you add a class to the link. If you want to do that, add the following code to your functions.php file:

add_filter('next_posts_link_attributes', 'posts_link_attributes');
add_filter('previous_posts_link_attributes', 'posts_link_attributes');

function posts_link_attributes() {
  return 'class="styled-button"';
}

For more on pagination, see Add Pagination to WordPress Templates.

Display Database Queries, Time, and Memory Usage

The following code displays in the footer the number of database queries the page executed, the time it took to run those queries, and the memory usage of the page.

function sv_performance( $visible = false ) {

    $stat = sprintf( '%d queries in %.3f seconds, using %.2fMB memory',
        get_num_queries(),
        timer_stop( 0, 3 ),
        memory_get_peak_usage() / 1024 / 1024
    );

    echo $visible ? $stat : "<!-- {$stat} -->" ;
}

add_action( 'wp_footer', 'sv_performance', 20 );

The result appears as a comment in the footer. Check the page source to see it:

<!-- 33 queries in 1.532 seconds, using 48.75MB memory -->

For more performance optimization, see Improve WordPress Loading Time.