Language

Get in touch

WooCommerce plugin · since 2014 · aka WOOF

HUSKY

A faceted product filter for WooCommerce: taxonomies, attributes, price, stock, meta fields and text, as a widget or shortcode, with page reload or AJAX.

First release
Active installs
80,000+
Rating
4.6 / 5 (336)Read reviews
  • PHP
  • WordPress
  • WooCommerce
  • WP_Query
  • WP_Tax_Query
  • WP_Meta_Query
  • MySQL
  • Transients
  • Object cache
  • jQuery
  • AlaSQL
  • PDO
  • WPML
  • Polylang
View on wordpress.org wordpress.org

About

HUSKY filters WooCommerce products by categories, attributes, tags, price, stock status, rating, author, SKU, custom meta fields and text. It has been on wordpress.org since 2014. The free and premium editions are built from one code base; the edition is selected by the major version number, and premium-only parts are either stubbed out or capped in the free build.

Every filter state ends up as a WordPress query over wp_posts, wp_postmeta and the term tables, and stores run tens of thousands of products and variations. Showing how many products each option would return means one more query per visible term on every page view. Variable products, catalog visibility, taxes and currency plugins each change what a match is.

The plugin also runs inside request handling it does not own: WordPress rewrite parsing, WooCommerce's main product query and templates, page builders, multilingual plugins and caching layers. Most of the code described below exists because one of those layers made the direct approach wrong or too slow.

HUSKY · products filter

what was solved

LIVEsince 2014

SEO URLs for filter states

in the product

What it givesFiltered pages get clean, stable addresses that search engines can index. The owner decides which filter pages appear in Google and with what title.

Problem
Filter states are query strings, which search engines either ignore or crawl without limit. Path URLs like /swoof/color-red/size-m/ are unknown to WordPress rewrite rules and conflict with canonical redirects.
Solution

Intercept request parsing, strip the filter segment, let WordPress match the rest, and rebuild the filter state from the segment with a fixed encoding and per-depth index rules.

Stack
  • WordPress rewrite API
  • PHP
  • JavaScript
  • Yoast SEO
Tasks · 6
/shop/swoof/color-red/size-xl/ matches no rewrite rule and gives 404Cut the filter segment, route the rest via core rewrite rules, rebuild filters from it

A filter link is the archive URL plus the filter slug (swoof by default) and one segment per filter: /product-category/shoes/swoof/color-red-and-blue/size-xl/. The plugin registers no rewrite rules. url_process() runs on do_parse_request on the front end and returns at once unless get_search_request() finds /<slug>/ in the path. It takes the filter segment up to /page/, ? or #, removes it, and matches the remainder (/product-category/shoes/ or /shop/page/2) against $wp_rewrite->wp_rewrite_rules() with code copied from WP::parse_request(): verbose page rules, public and private query vars, viewable post types, non-queryable taxonomies, wp_resolve_numeric_slug_conflicts() and the request filter. It then calls query_posts(), handle_404() and register_globals() (the parse_request action before WordPress 6.0) and returns false, so core does not parse the URL again.

parse_url_query() splits the segment by '/' and matches each part against the name map from get_all_items(): attribute slugs without pa_, WPML-translated taxonomy slugs, price, and the fixed words instock, onsale, featured, backorder_not_in. -and- and -or- become commas, -to- a range separator, + a space; price-10-to-50 becomes min_price and max_price. add_request() merges the result through the woof_get_request_data filter, so get_request_data() returns the same array as for a ?swoof=1&pa_color=red query string. On the main query parse_query() then sets post_type product, marks the query as a post type archive and adds the meta queries, and tax_query() on woocommerce_product_query_tax_query replaces WooCommerce's entries for the same taxonomies with the plugin's tax query.

Because the remainder is routed by the existing rules, the shop base, the product category base and archive pagination keep working without new rules: /page/N/ is cut off before the filter segment is located and stays in the routed path. parse_query() turns is_tax off on filter pages, so the current category is recovered from the last segment of the cleared URL (get_term_from_cleared_url()). When the shop page is also the front page, the redirect_canonical fix below keeps WooCommerce from redirecting the filter URL to /.

For a URL that matches an SEO rule, meta_head() prints the canonical: the filter URL without the query string if the state is indexable, with the Yoast and SEOPress canonicals switched off, otherwise the URL cut at the filter slug, which is the plain archive. With Yoast SEO, sitemap_index() on wpseo_sitemap_index appends the URLs an admin lists in a settings textarea, one per line; the premium build escapes them with esc_url_raw() and esc_xml().

The order in which filters are used should not change the URLWrite filters in sorted key order; mirror the -and- / -to- / + grammar in PHP and JS

woof_get_submit_link() in url_parser.js sorts the active filters by key and writes each as <name>-<values>, with ',' as -and-, a range separator as -to- and spaces as +. Price becomes price-<min>-to-<max>; in stock, on sale, featured and backorder become fixed words. parse_url_query() reverses the mapping.

Because of the sort, choosing color first and size second gives the same path as the other way round. Values inside one filter are written in the order they were selected.

Attribute pa_niveau and meta field niveau ask for the same URL nameDetect name collisions: taxonomy keeps the short name, the other gets a unique fallback

URL names drop the pa_ prefix, so an attribute and a meta key can produce the same name, and one map entry would hide the other. resolve_seo_key_conflict() gives the short name to the registered taxonomy regardless of the order of filters in the settings, so existing URLs stay valid, and derives a name for the other filter from its real key, adding a numeric suffix if needed. WPML-translated taxonomy slugs are resolved before this step.

Every recount query reads the filter state from the URL path againMemoize parse_url_query() per URL, but only after wp_loaded

get_request_data() is called from every recount query, and with SEO URLs each call goes through woof_get_request_data to parse_url_query(). With Dynamic Recount and Hide Empty (premium) one page can make more than a thousand such calls, so the parse result is cached per URL.

The cache is enabled only after wp_loaded, so a call made before taxonomies and settings exist cannot freeze an incomplete name map for the rest of the request.

Which filter pages to index, and with what title and metaIndex by depth and by regex rules with per-rule meta; skip broken patterns quietly

WOOF_SEO counts the active filters. Above index_deep (default 2) the page gets the 'Disable page indexing' setting (default yes, so noindex,nofollow through wp_robots) and rules are not checked. At or below it a matching rule makes the page indexable. Rules are URL patterns with {any} placeholders compiled to regular expressions, each with a title, meta description, H1 and SEO text that can include the selected term names, and are stored per locale. Canonical tags are printed only for URLs that match a rule.

In the premium build, preg_match runs under a temporary error handler, so a rule whose URL is not a valid expression is skipped and listed in an admin notice, and an empty rule is skipped. The free edition uses the first two rules only.

WooCommerce 11 redirected filter URLs on a shop front pageCancel only the redirect to home_url, and only for URLs that carry the filter slug

When the shop page is also the static front page, WooCommerce 11.0.0 sets it as the queried object on product archives. redirect_canonical() then treats /swoof/... as a non-canonical copy of the front page and redirects to /, dropping every filter. fix_front_page_shop_canonical() returns false only when the front page is the shop page, the path contains the filter slug and the redirect target is the home URL, so slash and slug normalization keep working everywhere else.

Dynamic recount and query caching

in the product

What it givesEach filter option shows how many products it will return, so shoppers avoid dead ends. Counts are cached, so the shop stays fast on large catalogs.

Problem
Each option in the filter shows how many products it would return given the current selection. That is one product query per visible term on every page view, and it is the main cost on large catalogs.
Solution

Counts come from a stripped-down ID-only query that reproduces the filter semantics. Results are cached per request, in a dedicated table and in transients, and the form can be deferred until a visitor asks for it.

Stack
  • WP_Query
  • MySQL
  • Transients
  • Object cache
Tasks · 6
A term's count must reflect the rest of the current selectionRebuild the query per term, swapping the term in by single or multi selection rules

dynamic_count() rebuilds the tax_query for each term. Terms already selected get no count. For radio and select (single) the term replaces the selected value of its taxonomy; for checkbox and multi-select (multi) it is added to it. Terms of the current category page and of the shortcode's taxonomies attribute are always ANDed in, so counts on a category page stay inside that category.

Per-taxonomy logic (OR, AND, NOT IN) from the settings, price and rating ranges, extension meta conditions, the WPML language and catalog visibility are applied the same way as in the product query. Extensions declare their recount behaviour through html_type_dynamic_recount_behavior, so color, image and label types reuse this code.

Hiding options with zero results is built on these counts: the term is counted and skipped when the count is 0, so it cannot be cheaper than showing counts. This switch is premium-only; the free edition forces it off in the views.

A count query should not load posts or run FOUND_ROWSSubclass WP_Query with found-rows and loop methods as no-ops, fetch IDs only

WP_QueryWOOFCounterIn extends WP_Query and overrides set_found_posts(), setup_postdata(), the_post() and have_posts(). The count query runs with fields=ids and nopaging, so MySQL returns a list of IDs, WordPress builds no post objects and issues no second FOUND_ROWS query. The count is the length of the list.

WooCommerce's popularity and rating posts_clauses filters are removed before the count query, because ordering does not matter for a count and those filters add joins to the SQL.

The same counts are recomputed for every visitor and pageStore counts in a key-value table keyed by md5 of the full query args, truncate on cron

With 'cache count data' on, WP_QueryWoofCounter hashes the JSON of the final query arguments, which already include the language and visibility parts, and looks the hash up in {prefix}woof_query_cache, a two-column table with the hash as primary key. Writes use INSERT ... ON DUPLICATE KEY UPDATE, so two page views writing the same key at once do not fail. A site with a persistent object cache can move the counts to wp_cache_* through the woof_use_wp_cache filter.

There is no cheap way to find which cached counts a new or edited product affects, so a scheduled event truncates the whole table on the configured period. If the MySQL user lacks the CREATE privilege, the settings page prints the SQL to run by hand and keeps the option off.

Price slider min/max query scans postmeta on big storesMemoize min/max per SQL hash per request and in a transient, clear it on product save

get_filtered_price() builds one SELECT MIN/MAX over _price joined with the current tax and meta query. The same range was requested several times per page (min and max for each price filter), so the result is memoized in a static array keyed by md5 of the SQL. With 'Optimize price filter' on, results also go into one transient for a day, and any woocommerce_update_product deletes it. The SQL passes through woof_get_filtered_price_query, so a site can add index hints.

Term trees are rebuilt from scratch for every taxonomy on every pageCache assembled trees in transients keyed by arguments and the current language

WOOF_HELPER::get_terms() loads all terms of a taxonomy with get_categories() and assembles the parent-child tree in PHP. The result is memoized per request and, with 'Cache terms' on, stored in a transient whose key includes taxonomy, hide_empty, depth flags and the current WPML or Polylang language, so each language has its own cached tree.

Clearing walks wp_options for _transient_woof_terms_cache_% and deletes each transient, since WordPress has no prefix delete for transients.

Recount runs even if the visitor never opens the filterRender a button instead of the form and fetch the form over AJAX on first click

With start_filtering_btn=1 the server outputs only a button, so no term queries run on page load. On click, front.js posts woof_draw_products with the product shortcode set to 'woof_nothing': the server renders the filter form and no products, the redraw zone is replaced and the widgets are re-initialised. The button is skipped when a search is already active, so a filtered page still shows its selection.

Hiding the form with CSS or display rules saves nothing, because the counts are computed on the server before the HTML is sent. Performance threads on the support forum often had to make this point.

Working inside WooCommerce's product query

in the product

What it givesFiltering keeps working with the shop's own theme, category pages, variable products, taxes and currencies, so shoppers never see items in sizes that are sold out.

Problem
The filter changes the main shop query and relies on WooCommerce templates it does not own. It has to keep working across WooCommerce data model changes, with variable products, catalog visibility, taxes and currency plugins.
Solution

Hook late into parse_query and the WooCommerce product query, restore the archive state templates expect, and handle each data model difference explicitly by version or by table.

Stack
  • WP_Query
  • WooCommerce
  • MySQL
  • jQuery
Tasks · 6
Filtered results must render through the theme's shop templateRewrite main-query flags at parse_query priority 9999 so it renders as product archive

When the search slug is in the request, parse_query() sets post_type=product and is_post_type_archive, and clears is_tax, is_tag, is_home, is_single and is_search. WooCommerce and the theme then render the shop loop for any filtered URL. Extensions add their meta conditions through assemble_query_params(), and per-taxonomy operators come from the woof_main_query_tax_relations filter.

The same hooks must not touch REST and Store API requests. The handlers return early on /wp-json/ URLs and parse_query is unhooked on rest_api_init, so block themes and headless clients get WooCommerce's own results.

Filtering on a category page loses the categoryCarry the term as really_curr_tax and keep it in a transient keyed by the visitor IP

A filter URL is no longer the term archive, so get_queried_object() stops returning the category. When the archive is rendered, set_really_current_term() stores the term through WOOF_STORAGE, which the plugin runs in transient mode: one transient per visitor, keyed by a hash of the visitor IP, kept for a day. The JS also appends really_curr_tax=<term_id>-<taxonomy> to the filter link, and get_really_current_term() falls back to it when nothing is stored. The term is then ANDed into the product query, the recount queries and the text search. AJAX requests read the stored term but never overwrite it.

Catalog visibility is stored differently by WooCommerce versionBranch by version: _visibility meta before 3.0, product_visibility NOT IN after

WooCommerce 3.0 moved the visibility, featured and out-of-stock flags from postmeta to the product_visibility taxonomy. product_visibility_not_in() turns the needed flags into term_taxonomy_id NOT IN clauses. generate_visibility_keys() adds outofstock when the store hides out-of-stock items; when the 'listen catalog visibility' option is on, it also adds exclude-from-search always and exclude-from-catalog outside a search. Older stores get a _visibility meta condition instead.

The same rules are applied to the main query, the shortcode query, recount queries, text search and turbo-mode data. If one of them differs, counts and results disagree.

Variable product shown while the chosen size is out of stockFind parents of sold-out matching variations in the attribute lookup table, exclude

A variable product counts as in stock if any variation is, so a plain query for 'in stock + size M' returns a shirt whose M variation is sold out. When the in-stock filter is combined with attribute filters, posts_where() collects the selected pa_* terms, including the term being counted during recount, and runs two grouped queries on wc_product_attributes_lookup: parents with an out-of-stock matching variation, and parents with an in-stock one. The difference is appended as ID NOT IN.

An older mode, selectable in the in-stock filter settings, queries product_variation posts by attribute meta and _stock_status instead of the lookup table. It is slower, and by default it excludes every parent that has a sold-out matching variation; parents that also have an in-stock match are kept only when the woof_exclude_existing_variations filter returns true.

Price bounds differ from stored prices under tax and currencyConvert bounds back to base currency and strip inclusive tax before the meta query

_price is stored in the shop's base currency and, on many stores, without tax. If the visitor uses a WPML (WCML) currency, assemble_price_params() converts the bounds back to the base currency. If prices are displayed with tax but stored without, the inclusive tax for the standard rate is subtracted from both bounds. For the slider's upper end, get_max_price() finds the product holding the maximum price and asks WooCommerce for its price including tax.

With FOX (WOOCS) the displayed slider values are converted through woocs_exchange_value while the query keeps base-currency numbers.

AJAX mode: redraw results, counts and page parts without a page reloadOne AJAX endpoint renders products and form via shortcodes; JS swaps zones, pushes URL

woof_draw_products() rebuilds $_GET from the link the visitor would have opened, renders [woof_products] and [woof] with that state, and returns products, form and extra fragments (result count, ordering, pagination) as JSON. front.js replaces each fragment, puts back fragments the response did not include, re-initialises the widgets and calls history.pushState so the URL can be shared. onpopstate reloads the page when the query string changed. A lock flag drops clicks while a request is running.

The shop loop is wrapped by hooking woocommerce_before_shop_loop and woocommerce_after_shop_loop. Themes that do not fire both hooks break AJAX mode, which explains a family of support threads with different symptoms and one cause.

Turbo mode: filtering in the browser

in the product

What it givesOn large catalogs, filtering runs in the shopper's browser instead of the server. Results appear quickly and the server carries less load during busy hours.

Problem
On large catalogs every filter click costs a set of tax and meta joins on the server, plus one count query per visible term.
Solution

Precompute a flat JSON index of products and variations, load it in the browser and run the filter as SQL over it with AlaSQL. The server only renders the matching IDs. Included in both editions.

Stack
  • PHP
  • JSON
  • AlaSQL
  • JavaScript
  • Cron
Tasks · 5
Product data is spread over posts, postmeta, terms and variationsFlatten each product and variation into one JSON record: prices, terms, meta, stock

get_data_by_id() writes ID, title, SKU, author, parent, stock status, the term slugs of every filterable taxonomy (variation attributes for variations), the configured meta fields and a list of regular/sale price pairs. A variable product stores one pair per child, so the browser can compute its price range with the same rule WooCommerce uses: the sale price counts only when it is lower than the regular price.

Prices are stored with tax when the shop displays tax-inclusive prices, and FOX is switched to the default currency before reading them, so the index is in one currency.

Indexing thousands of products hits PHP time limitsBuild in 10-product AJAX steps, one file per WPML language and per multisite blog

create_data_search_files() takes an offset, processes 10 IDs of products and variations with the visibility rules applied, merges them into the JSON file and returns the next offset; the admin script loops until it gets -1. WPML is switched to the default language and each product is written into the file of every language through its translation ID. check_id_file() adds the blog ID on multisite so sites do not share a file.

The default cap is 15,000 records, adjustable with the woof_turbo_mode_products_limit filter.

A scheduled rebuild must not serve a half-written indexOwn cron runner handles ID slices per request, swaps a _temp file in at the end

PN_WP_CRON_WOOF_TURBO_MODE keeps the event state (full ID list, position, next run) in an option and runs on init. Each process() call handles one 10-ID slice, and a 5-second throttle stored in woof_cron_limit_do keeps a burst of page views from running slices back to back. Slices are written to data_<id>_temp_<lang>.json; when the list is exhausted, do_recreate_file() moves the temp file over the live one, and visitors keep the old index until the new one is complete.

The cost of the design is staleness: products added after the last rebuild are invisible to the filter, and the rebuild depends on site traffic the same way WP-Cron does.

Filter state has to become a query the browser can runGenerate AlaSQL WHERE clauses with custom functions for price ranges and visibility

turbo_mode.js turns the current values into a WHERE string: taxonomy membership through indexOf on the term arrays, meta comparisons, LIKE for SKU and title, BETWEEN for sliders and dates. Custom AlaSQL functions compute a product's min and max price from its variation list and apply catalog visibility. For variable products the in-stock filter first selects parents of out-of-stock variations that match the other filters. Recount runs over the same in-memory table.

The resulting IDs are posted to woof_draw_products as turbo_mode_ids, and the server renders a post__in query instead of the filter joins.

The browser needs the settings, but they hold server secretsStrip statistics DB credentials and cron secret keys from the localized settings

The JS rebuilds the filter logic from woof_settings, so it needs most of that option on the page. get_public_settings() removes the statistics block, which contains the external MySQL credentials, and every cron_secret_key, and passes the rest through a filter for further removal. In the premium build the rebuild endpoint requires a nonce and manage_woocommerce.

Search statistics

in the product

What it givesThe owner sees what shoppers actually filter and search for, without slowing the shop or filling its database with an ever-growing log.

Problem
Recording what shoppers filter by means writing on the hot path of every filtered page view, from anonymous visitors and bots, often on shared hosting.
Solution

Write one small raw row per request into a staging table, and keep long-term statistics on a separate MySQL server that the extension requires.

Stack
  • MySQL
  • PDO
Tasks · 2
Logging must not multiply writes or be flooded by one clientLog once per request, dedupe by request+IP+day hash, cap staged rows per IP

The statistics extension hooks woof_get_request_data, which runs many times per page; a static flag limits logging to the first call, and AJAX calls other than the products render are ignored. Only the filter keys chosen in settings are kept, the request is cut to request_max_deep keys (default 5) while the price range is preserved, and it is hashed together with the IP and the date.

A row is inserted only if that hash is new and the IP has fewer than user_max_requests rows (default 10) in the staging table.

Statistics grow without limit inside the shop databaseStore the statistics on a separate MySQL server over PDO, one table per month

The extension needs its own MySQL server, set in the options. init_pdo() connects to it with PDO, since $wpdb cannot hold a second connection with other credentials. place_statdata_into_db() creates a table per month named <year>_<month> and inserts the rows with prepared statements.

Reports query the monthly tables by date range, so the shop database does not hold the long-term statistics.

Hardening and settings integrity

in the product

What it givesAnonymous visitors cannot use the filter to run code or reach hidden data, and saved settings are not lost or overwritten when changes are made.

Problem
The filter takes query strings and AJAX posts from anonymous visitors, renders shortcodes and templates named in those requests, and stores settings that contain HTML, JavaScript and SQL. The changelog credits outside researchers with a series of security reports from 2022 to 2026.
Solution

Treat every request value as data: allow only known shortcodes and templates, sanitize settings recursively by key path, require capabilities for anything that stores code, and merge settings instead of overwriting them.

Stack
  • PHP
  • WordPress sanitization API
  • WPML
  • Polylang
Tasks · 5
AJAX requests carry the name of a PHP template to renderAccept only template names first seen in page content; AJAX cannot add new ones

[woof_products custom_tpl=...] is legitimately passed back in AJAX calls, so the value can neither be trusted nor dropped. sanitize_custom_tpl() keeps only letters, digits, '_', '-' and '/', which removes dots and traversal sequences. A name rendered from real page content is added to an allowlist option (last 20 kept); during AJAX only names already on that list are accepted.

Include paths built from view type names go through safe_view_slug(), which rejects anything that is not a plain slug.

AJAX requests carry shortcode text that must not run arbitrary shortcodesExtract only the plugin's own tags from the posted text with get_shortcode_regex

woof_draw_products() receives the products and form shortcode text from the page so the AJAX output matches the original call. check_shortcode() builds get_shortcode_regex() for the plugin's tags only ([woof_products] or [woof], plus the plugin's product shortcode aliases) and keeps the first match; anything else in the string is discarded before do_shortcode().

Settings mix HTML, JavaScript, slugs and plain text at any depthSanitize recursively; classify each field by its key or its path in the settings tree

sanitize_array() walks the settings tree, stores each value under its sanitized key and classifies every field by name or by its path in the tree: SEO rule text as HTML through wp_kses_post, descriptions as textarea, keys used in include paths as strict slugs, everything else as plain text. Path rules are needed because keys deep in the tree, such as text or description, are too generic to list globally.

The 'JavaScript after AJAX' setting can be changed only by users with unfiltered_html.

The AJAX response injects HTML fragments, and the request is rebuilt from the visitor's URLDrop woof_redraw_elements from the parsed link (premium); pass every fragment through wp_kses

AJAX rendering rebuilds $_GET from the page link sent by the browser, and the response injects woof_redraw_elements into the page as HTML, so a crafted link must not be able to supply that key. In the premium build the key is removed from the parsed link. Legitimate fragments (SEO title and H1, result count, ordering) are added by server-side filters, and every fragment passes through wp_kses before it is returned.

Term colors and images should be set once for all languagesMerge submitted per-term colors over stored ones; fall back to default-language term

The settings page lists only the terms of the current WPML or Polylang language, so a save posts colors for those terms only. woof_save_options() merges the stored color, color_img and images_term_* entries under the submitted values, so the other languages keep theirs.

A translated term with no color of its own uses its default-language original, found by get_original_language_term(). It reads the slug straight from wp_terms, because with WPML's ID adjustment get_term() may return the translated term again.

Debugging in the wild

support forum

What it givesProblems on a live shop get found at their real cause, even when it is the theme, the server, a cache or another plugin.

Problem
A report from a live shop names a symptom, not a cause. The same symptom can come from a theme template, a cache or optimization layer, the server, another plugin or the filter itself.
Solution

Reproduce on a clean install or a copy of the site, rule out one layer at a time, and fix where the cause is: in the plugin for everyone, or with a targeted change on that site.

Stack
  • Browser DevTools
  • Nginx
  • LiteSpeed Cache
  • jQuery
  • ACF
  • MySQL
Tasks · 5
Subcategory toggles vanish and filtering breaks whenever the URL has any query stringIsolated the fault to the server with an arbitrary GET parameter test, then the Nginx rewrite was fixed

Even a meaningless parameter such as ?dwdadd=1adw broke the page, which proved the fault was in query-string handling, not in the filter. The Nginx config was inserting a stray ? after php and dropping every parameter, so the filter never received its arguments.

Shop stops refreshing after filtering once LiteSpeed Cache is installedLogged the AJAX response to expose a JSON parse error, then bisected LiteSpeed settings down to CDN Mapping

The response arrived but JSON.parse failed with a bad escaped character, meaning LiteSpeed's CDN Mapping was rewriting URLs inside it. Turning CDN Mapping off restored filtering.

Filter and Reset buttons update the URL but the product list never reloadsTraced it to the slim jQuery build, restored full jQuery and replaced the host's modified WordPress core with a clean install

The site loaded jQuery slim, which has no AJAX module, so the request for filtered results was never sent while the URL still changed client-side; disabling every other plugin changed nothing. The slim build came with a modified WordPress core from the host's auto-installer; restoring full jQuery and reinstalling core fixed it.

Meta sliders on small decimal ACF values (around .005) filter nothing or everythingAdded the woof_slider_meta_query_type filter so the SQL cast can be set to DECIMAL per meta key

The reporter rebuilt the field as a plain custom field and the fault stayed, so ACF was not the cause; support reproduced it. The meta query compared the values without a DECIMAL cast, and the plugin got the woof_slider_meta_query_type hook, which receives $type and $key.

Filter freezes after the browser Back button; Search does nothingAdded a pageshow listener that resets the submit lock and popup on bfcache restore, shipped in updates

Pages restored from the back/forward cache kept woof_submit_link_locked set and the loading popup open. The listener checks event.persisted or navigation type 2 and resets both; the change went into the plugin.

Numbers

wordpress.org
  • Active installs 80,000+ wordpress.org counter.
  • Reviews 336 Average 4.6 out of 5.
  • Years supported 12 Since 1 December 2014, last update 9 October 2026.