Plugin documentation

Category Products Reorder for WooCommerce

Drag and drop the products of each WooCommerce product category into your own order. The order is used on category pages whenever the default sorting is active, on classic and block themes, with a separate order per Polylang language.

Version 1.5.0 WooCommerce plugin Paid plugin Requires WordPress 6.5 Requires PHP 7.4 Tested up to 7.1 Updated 6 Oct 2026

1. Overview

Category Products Reorder for WooCommerce lets you decide which products shoppers see first on each product category page. You open WooCommerce → Reorder, choose a category and drag its products into the order you want. Every drag, and every use of the move buttons, is saved automatically.

Each category keeps its own order, so one product can be first in "Sale" and last in "Shoes". The order is stored in a small table of the plugin's own ({prefix}pro_sort). Your products and their global menu order are never changed. On the shop, the saved order is used when WooCommerce is sorting by its default sorting. When a shopper chooses price, popularity, rating or latest, that sorting is used instead. Products that have no saved position come after the ordered ones, in WooCommerce's usual default order.

The plugin works with classic themes such as Storefront and with block themes. It does not look for particular templates. It recognises a product query that is limited to one product category and uses the default sorting, wherever that query comes from (see How the order is applied on the shop).

How it works

  1. You choose a category on the Reorder screen. The screen lists the products of that category and its sub-categories, with the saved order first.
  2. You move products. About half a second after your last move the whole list is sent to the server and saved for that category.
  3. When a visitor opens that category with the default sorting, the plugin adds one join to the product query and puts the saved positions first. Products without a position follow.

At a glance

Order per category

Every product category has its own order. Products of sub-categories that appear on a category page can be positioned in the parent category's order.

Drag, buttons or keyboard

Drag the handle, use the move up, down, top and bottom buttons, or use the arrow keys, Page Up, Page Down, Home and End. Touch screens work too.

Search and large lists

Search by name, SKU or ID. Large categories open at once: the first 40 rows are drawn straight away and the rest load in the background.

Plan ahead

Draft, pending, private and scheduled products are listed with a label so you can position them before they go live.

Polylang

Every language has its own categories and so its own order. "Copy order to translations" applies one language's order to the others.

Safe saving

A save replaces the whole order of one category in one database transaction, one save at a time per category. A failed save keeps the previous order.

What it does not do

  • It does not change product menu order, titles, dates or any product data.
  • It does not override a shopper's choice of price, popularity, rating or latest sorting, and it does nothing when a store's own default sorting is something other than "Default sorting". See How the order is applied on the shop.
  • It does not apply to the main shop page, tag pages, search results or any query that is not limited to exactly one product category.
  • It does not order the product list in the WordPress admin.
  • It has no settings screen. The only choices are the ones on the Reorder screen. See Settings for the few limits you can change in code.
  • It does not clear page-cache plugins. See Caching.

2. Requirements

ItemRequirementNotes
WordPress6.5 or laterFrom the Requires at least line. Tested up to 7.1 (from readme.txt). Core behaviour described on this page was checked against WordPress 7.1.2.
PHP7.4 or laterFrom the Requires PHP line.
WooCommerce8.0 or later (header: WC requires at least: 8.0)Required. The plugin header says Requires Plugins: woocommerce. In WordPress 7.1.2 that makes WordPress refuse to activate the plugin while WooCommerce is not installed and active ("Error: <plugin name> requires 1 plugin to be installed and activated: ..."). The plugin itself does not check the WooCommerce version at run time. Tested up to WooCommerce 11.1 (header WC tested up to). WooCommerce 11.1.2 was checked for the behaviour described here.
Polylang or Polylang ProOptionalAdds language groups in the category list and the "Copy order to translations" button. Without it the plugin works with one order per category. See Polylang.
Who can use the screenCapability manage_woocommerceWooCommerce gives this to Administrators and Shop Managers (checked in WooCommerce 11.1.2). The screen and every AJAX action check it.
DatabaseMySQL or MariaDB with a table-creation rightThe plugin creates one table. Saving uses a database lock (GET_LOCK) and a transaction. The all-or-nothing behaviour needs a transactional storage engine such as InnoDB, which the plugin does not choose for you (it uses your database default).
BrowserJavaScript onThe Reorder screen is built in the browser with jQuery UI Sortable and jQuery UI Touch Punch, both shipped with WordPress. No external files are loaded.

3. Installation

  1. Make sure WooCommerce is installed and active.
  2. Open Plugins → Add Plugin → Upload Plugin (in WordPress 7.1.2 the menu item is called "Add Plugin"), choose the zip file from your purchase, click Install Now and then Activate.
  3. Open Plugins → WpExperts Hub Licences and activate your licence key to receive updates (see below). The plugin works without a licence.
  4. Open WooCommerce → Reorder, choose a category and drag the products.

What activation creates

  • The database table {prefix}pro_sort (for example wp_pro_sort), created with WordPress's dbDelta() (see The table for its columns).
  • The option wooexp_reorder_db_version, set to 2.
  • No roles, no scheduled events, no pages, and no changes to products.

Plugin updates do not run the activation routine, so the plugin also checks the stored wooexp_reorder_db_version on every admin page load and creates or upgrades the table when it differs from the plugin's own version of the table (currently 2). After an update, the first admin page you open brings the table up to date. Saved orders are kept.

Licence and updates

The plugin bundles the WpExperts Hub licence and update client (licence/class-wpxh-licence-client.php, client version 2.0.0). One screen manages every WpExperts Hub plugin on the site: Plugins → WpExperts Hub Licences. It needs the manage_options capability. The client is registered every time WordPress loads this plugin (also outside the admin, so scheduled update checks work).

  1. Find your licence key in the purchase email, or under My Account → Downloads on wpexpertshub.com.
  2. Open Plugins → WpExperts Hub Licences. Each WpExperts Hub plugin has a card with its status ("Active" or "Not active"), the installed version and, when known, "version X is available".
  3. Paste the key into Licence key and click Activate licence. Characters other than letters, digits and hyphens are removed from the key before it is sent.
  4. To move the plugin to another live site, click Deactivate licence first ("Deactivate before moving the plugin to another live site.").
  5. If you cannot find your key, open "Can't find your key? Email it to me", enter the email address used for the purchase and click Send licence key. The licence server sends the email.
  • Updates come through WordPress. Whenever the licence server reports a newer version than the installed one, the plugin is added to WordPress's own update list, so it appears on the normal Dashboard → Updates and Plugins screens. When an update exists but the server returns no package for your site, WordPress shows "Automatic update is unavailable for this plugin. Activate your licence to enable updates." under the plugin.
  • The update information is cached. The answer of the licence server is kept in the site transient wpxh_licence_check_v2 for 12 hours. After a failed request the previous answer is kept and the check is retried after one hour. The cache is cleared when you activate or deactivate a licence, send a key request, or when WordPress finishes any install or update (the hook upgrader_process_complete).
  • A reminder on the Plugins screen. Users with manage_options see "Activate your WpExperts Hub licence to receive updates for: ..." with a Manage licences link on Plugins while any registered WpExperts Hub plugin has no active licence.
  • Links in the plugin row. The plugin adds Reorder products and the licence client adds Licence (for users with manage_options) under the plugin name on the Plugins screen.
  • The licence key only unlocks updates. No code of the plugin checks the licence status before ordering products or showing the Reorder screen. The status is used only on the licence screen and in the Plugins screen notices. Whether the server returns an update package for a site is decided on the licence server.

What the licence client sends to wpexpertshub.com, and when, is listed under Data sent to wpexpertshub.com.

Updating

Update from Dashboard → Updates (needs an active licence) or upload the new zip over the old plugin. Saved orders are kept. The table is checked at the next admin page load (see above). The 1.5.0 upgrade notice in readme.txt says saved orders are kept.

Multisite

The plugin has no network admin screen. Each site has its own Reorder screen, its own table ({prefix}pro_sort with that site's prefix) and its own wooexp_reorder_db_version option. Activation creates the table for the site on which it runs; every other site creates its table the first time an admin screen loads there. Deleting the plugin removes the table of every site (see What deactivation and uninstall remove).

Deactivating and deleting

Deactivating keeps the table and all saved orders, but the orders are no longer applied on the shop, because the query filter is gone. Reactivating applies them again. Deleting the plugin removes the table and the version option. It does not remove the licence options, so deactivate your licence first if you want the activation released.

4. Quick start

  1. Check Appearance → Customize → WooCommerce → Product Catalog (classic themes). Default product sorting should be "Default sorting (custom ordering + name)". The Reorder screen warns you if it is not (see How the order is applied on the shop).
  2. Open WooCommerce → Reorder. You can also open Products → Categories and click Reorder products under a category.
  3. Choose a category in Product category.
  4. Drag a product by its handle (the three bars), or use the arrows at the end of the row. Watch the status text: "Saving…" then "Order saved.".
  5. Click View category to open the category page in a new tab. With the default sorting, the products appear in your order.

5. Features

The Reorder screen

The screen is at WooCommerce → Reorder (address admin.php?page=wooexp-reorder). The page title is "Reorder Products". It needs manage_woocommerce. At the top it says: "Select a product category, then drag and drop the products into the order you want. The order is saved automatically and is used on the category page when the default sorting is selected."

If your store's default sorting (the option woocommerce_default_catalog_orderby) is anything other than menu_order, a yellow notice adds: "Your store sorts products by something other than "Default sorting" at first. The custom order below only applies when visitors use the default sorting. Choose "Default sorting (custom ordering + name)" under WooCommerce > Settings > Products to use it from the start." The link in that notice opens the WooCommerce Products settings tab. In WooCommerce 11.1.2 the setting is not on that tab: WooCommerce 11.1.2 offers it in the Customizer, under WooCommerce → Product Catalog → Default product sorting. If your theme has no Customizer, the value is stored in the option woocommerce_default_catalog_orderby, and WooCommerce lets code change it with the filter of the same name.

The screen has these parts:

  • Product category drop-down with the first entry "Select Category".
  • A View category link next to the label (opens the category page in a new tab).
  • A search box, "Search by name, SKU or ID", disabled until a category is open.
  • Two buttons that appear only when they apply: Copy order to translations and Reset to default order.
  • A status text that reads "Saving…", "Order saved." or an error message.
  • A summary line (number of products and a "Custom order" or "Default order" badge) and the sortable list.

Choosing a category

The drop-down lists every product category, including empty ones. Parents come before their children, siblings are sorted by name, and each child is indented with a dash for each level (the list goes down to 12 levels below the top level). After each name, in brackets, is the category's product count as stored by WordPress. It is not the number of rows on the list, because the list also contains products of sub-categories and products that are not published.

With Polylang the categories are grouped by language (see Polylang). Without it there is a single group.

Two ways to open a category directly: the Reorder products link under a category in Products → Categories, which opens the screen with &cat=<category ID>, and the same address typed by hand. If the browser remembers the previous choice in the drop-down, that category opens when the screen loads. A category with no products shows "There are no products in this category." with the View category link still available.

The product list

The list contains every product of the category and of its sub-categories whose status is Published, Private, Draft, Pending or Scheduled. Trashed products are not listed. Products that have a saved position come first, in that order. All others follow, sorted by WooCommerce menu order and then by title.

Each row shows:

  • A position number (the first row is 1).
  • The drag handle, then a 48 by 48 pixel product image.
  • The product name (a link to the product's edit screen, opening in a new tab), its ID and, if it has one, its SKU, for example "(#123, SKU-1)".
  • The post status as a small label (for example Draft, Pending, Private, Scheduled) for products that are not published.
  • "Out of stock" or "On backorder" when the stock status is one of those.
  • "Hidden" when the product's catalogue visibility is "Hidden" or "Search results only". Its tooltip says "This product is not listed on category pages." Hidden products are listed so you can order them, but visitors do not see them on category pages.
  • The price, and four buttons: Move to top, Move up, Move down and Move to bottom. The buttons that cannot move the product (for example "up" on the first row) are disabled.

A large category opens at once. The first 40 rows are sent with the first response. The remaining rows load in the background, 50 at a time, in the order they appear, and show a placeholder with the product ID until they arrive. You can start moving products before all rows have loaded.

Product rows come from a template, include/product.php. A theme can override it (see Template override).

Moving products

  • Drag. Drag a row by its handle. A placeholder shows where it will land, and the list scrolls when you drag near the edge. Touch screens work through jQuery UI Touch Punch.
  • Buttons. Click Move to top, Move up, Move down or Move to bottom on a row.
  • Keyboard. The handle is a button. Tab to it and press Up arrow or Down arrow to move one place, Page Up or Page Down to move ten places, Home to move to the top and End to move to the bottom. Combinations with Alt, Ctrl or the Command key are ignored. Screen readers hear "<product> moved to position X of Y." The handle's label reads "Move <product>. Use the arrow keys, Home and End."
  • Search. Typing in the search box hides rows whose name, SKU or ID does not contain your text (case is ignored; the ID is also matched with a leading #). The summary then reads "X of Y products shown" with the note "Dragging is paused while the search is active. Use the buttons to move a product, or clear the search." The buttons keep working, and they move a product within the full list, including the rows hidden by the search. If nothing matches, the list says "No product matches your search."

Saving

  • Automatic. 450 milliseconds after your last move the plugin sends the complete list of product IDs, in the current order, for the category. The status shows "Saving…" and then "Order saved.". If you move something while a save is running, a second save follows when the first has finished.
  • The whole list is stored. A save replaces all saved positions of that category with the list on screen, numbered from 0. After your first move in a category every product on its list has a saved position, including products you did not touch.
  • Only products of the category are stored. The server keeps only IDs that are products belonging to the category or its sub-categories, in the order given. IDs of pages, deleted products and products of other categories are dropped. A list in which none is valid is refused with "None of the products belongs to this category." and the old order is kept.
  • One save at a time. A save takes a database lock for that category (GET_LOCK, waiting up to 15 seconds) and runs inside a transaction: the old rows are deleted and the new ones inserted, or nothing changes. A second save of the same category waits, and the last one wins. If the lock cannot be taken in time the save fails with "Another save of this category is still running. Please try again." If the database cannot provide the lock function at all, the save goes ahead without the lock.
  • Size limit. One save accepts up to 20,000 products (changeable, see Settings). A longer list is refused with "This category has too many products to be ordered in one go." The list is sent as one comma-separated value, so PHP's max_input_vars does not cut it.
  • When a save fails. The status shows the reason (or "The order could not be saved. Please reload the page and try again.") and a Try again button. Your list stays on screen.
  • Leaving the page. If a change has not been saved yet, the browser asks you to confirm before you leave the page.
  • After a save the plugin refreshes WordPress's cached product queries and fires the action wooexp_reorder_saved (see Caching).

Reset to default order

Click Reset to default order (visible only while the category has a saved order) and confirm "Remove the custom order for this category and go back to the default WooCommerce sorting?". The saved rows of that category are deleted, the list reloads in WooCommerce's default order and the status reads "Custom order removed.". The category's page uses the normal WooCommerce order again. Products are not touched.

Polylang

Polylang counts as active for this plugin when the functions pll_get_term_language, pll_get_post and pll_get_term_translations exist.

  • One order per language. Polylang gives every language its own product categories. The order is saved per category, so every language has its own order. On the shop, the query of a category page names the category of the current language, so it uses that language's order.
  • Categories grouped by language. The drop-down shows one group per Polylang language (the language name as the group label), in Polylang's language order, then a group "No language" for categories without a language. The list includes all languages, whatever the admin language filter says (a fix in 1.5.0).
  • Products of all languages. The list of a category includes its products whatever the admin language filter is set to.
  • Copy order to translations. The button appears when the open category has a saved order and has translations. It asks "Copy this order to N translated categories? Their current custom order is replaced." For each translated category the plugin replaces every product of the saved order by its translation in that language (products with no translation are left out), keeps only the products that belong to that translated category or its sub-categories, and saves the result as that category's order. If nothing can be mapped for a language, that language's existing order is left unchanged. A message such as "Order saved. fr: 12, de: 0" reports how many products were saved for each language. Products of the target category that are not in the copied list lose their saved position and follow the ordered ones.
  • Without Polylang the copy action refuses with "There are no translations to copy the order to.".

How the order is applied on the shop

The plugin changes the SQL of product queries through the posts_clauses filter at priority 99. It runs these steps in this order. When a check fails, the query is left exactly as it was:

  1. Not an admin page. Queries on admin screens are skipped, except admin AJAX requests. Front-end pages, REST requests and admin AJAX requests are all eligible.
  2. Not a search. A query with a search term (s) is skipped.
  3. Not opted out. A query that sets the query variable wooexp_skip is skipped. The plugin uses this itself for its own admin list.
  4. Default sorting. The query counts as default sorting when its orderby is empty, or is an array that has a menu_order key (for example array( 'menu_order' => 'ASC' )), or is a string containing menu_order (WooCommerce turns "Default sorting" into menu_order title). Price, popularity, rating, latest, random and title sorting all fail this check, so shoppers can still use them.
  5. A product query. The post type includes product, or no post type is set and the query is a product category archive.
  6. Find the one product category. The query's taxonomy conditions (nested ones included) must contain exactly one product_cat condition with exactly one term and the operator IN. The term can be given by ID or by another field, such as the slug. Conditions on other taxonomies (attribute filters, for example) do not matter. When the query does not have exactly one such condition, the category found is 0.
  7. The category filter. The category found (or 0) is passed through the filter wooexp_reorder_query_term_id. A code snippet can return another category ID, or 0 to stop here.
  8. A category with a saved order. The category must not be 0 and must have at least one saved row (checked once per request and category).

When all checks pass, the plugin adds this join and sort. The saved rows of the category are joined once, and the new sort comes first, followed by whatever sort the query already had:

LEFT JOIN (SELECT pro_id, MIN(menu_order) AS wooexp_order
           FROM {prefix}pro_sort WHERE term_id = <category> GROUP BY pro_id) wooexp_sort
       ON wooexp_sort.pro_id = {prefix}posts.ID
ORDER BY wooexp_sort.wooexp_order IS NULL ASC,
         wooexp_sort.wooexp_order ASC,
         <the query's own ORDER BY>

In plain words: products with a saved position come first, lowest position first. Products without one follow, in the order the query would have used anyway (for the default sorting, WooCommerce's menu order and then title). New products, and products you added to a category after you last saved it, therefore appear after the ordered ones until you place them. Because the order is part of the SQL, it carries across pages of a paginated category.

Where this happens

  • Classic category pages (for example in Storefront). WooCommerce's category query uses one product_cat term and, with the default sorting, menu_order title. When the store's default sorting is "Default sorting", category pages open in your order, and a shopper who picks "Default sorting" in the sort menu gets it again. In WooCommerce 11.1.2 that menu entry is shown only while the store's default sorting is "Default sorting"; with another store default, the address parameter ?orderby=menu_order still selects it.
  • Sub-categories. A parent category's page also lists products of its sub-categories. The parent's own saved order controls that page, and it can contain products from its sub-categories (the Reorder screen lists them). A sub-category's page uses the sub-category's own saved order.
  • Shortcodes. [product_category category="slug"] uses menu_order title as its default sorting in WooCommerce 11.1.2, so it follows your order. [products category="slug"] without an orderby attribute follows the store's default sorting, so it follows your order when that is "Default sorting", and so does orderby="menu_order". A shortcode with two or more categories is not touched, because the query then has more than one category term.
  • The WooCommerce Store API. A products request with orderby=menu_order and a single category passes the steps above. The Store API's own default is orderby=date, which does not.
  • Block themes. The plugin does not look for blocks. Any product query from a block, such as the Products by Category template or a Product Collection block, is ordered when it passes the steps above. The plugin's readme lists the Products by Category template, Product Collection blocks inheriting the archive query, and Product Collection blocks filtered to a single category with the default sort. This page did not trace the block code, so test your own block theme.

When your order is not used

  • The shopper chose another sort, or the store's default sorting is not "Default sorting".
  • The page is not limited to one category (the main shop page, a tag, two categories at once).
  • The query is a search.
  • The category has no saved order.
  • A code snippet filters the category to 0 or sets wooexp_skip.
  • A different plugin changes the ORDER BY after priority 99.

Clean-up of saved rows

  • When a product is deleted permanently (hook deleted_post), its rows are removed from every category. Moving a product to the Trash does not remove them.
  • When a product category is deleted (hook delete_product_cat), its rows are removed.
  • A product removed from a category is not removed from that category's saved rows. It simply stops being on the category's page. If you add it back, its old position applies again. The next save of that category replaces the rows with the products on the list.

Caching

After every save, reset and copy, the plugin calls WordPress's wp_cache_set_posts_last_changed(), so product queries held in the object cache are refreshed. It then fires wooexp_reorder_saved.

Page-cache plugins keep their own copies of the category page, and the plugin does not clear those. Clear them yourself after reordering, or hook your cache plugin to the action. Example for LiteSpeed Cache (the hook litespeed_purge_url was found in that plugin's source on the live copy):

add_action( 'wooexp_reorder_saved', function ( $cat_id, $ids ) {
	$url = get_term_link( (int) $cat_id, 'product_cat' );
	if ( ! is_wp_error( $url ) ) {
		do_action( 'litespeed_purge_url', $url );
	}
}, 10, 2 );

Other places the plugin appears

  • Plugins screen. A Reorder products link under the plugin name opens the Reorder screen. The licence client adds a Licence link.
  • Products → Categories. A Reorder products row action under each category (for users with manage_woocommerce).
  • WooCommerce screen styles. The Reorder screen is registered as a WooCommerce screen so that WooCommerce's admin styles load.
  • WooCommerce compatibility. The plugin declares itself compatible with High-Performance Order Storage (custom_order_tables), the Cart and Checkout blocks (cart_checkout_blocks) and the product block editor (product_block_editor). It does not touch orders or checkout.

6. Settings

The plugin has no settings screen and registers no settings. What you choose is made on the Reorder screen (the category and the order). The table lists the one WooCommerce setting that decides whether your order is used, and the limits that can be changed in code.

SettingKeyDefaultWhat it does
Default product sorting (a WooCommerce setting, not this plugin's)Option woocommerce_default_catalog_orderbymenu_order ("Default sorting (custom ordering + name)")Your order is used at first only when this is menu_order. The plugin reads it to show the yellow notice on the Reorder screen. Other values WooCommerce offers: popularity, rating, date, price, price-desc.
Most products one saved order can holdFilter wooexp_reorder_max_products20000A save with more IDs is refused with HTTP 413 and "This category has too many products to be ordered in one go." Return a whole number.
How long a save waits for the category lockFilter wooexp_reorder_lock_timeout15 (seconds)Passed to the database's GET_LOCK. Values below 0 count as 0. If the lock is not taken in time the save fails with HTTP 409.
Rows loaded per background requestFixed in the code (ROWS_PER_REQUEST)50Not changeable.
Rows sent with the first responseFixed in the code (FIRST_ROWS)40Not changeable.
Delay before an automatic saveFixed in js/reorder.js450 millisecondsNot changeable.
Skip the order for one queryQuery variable wooexp_skipNot setSet it to true in WP_Query arguments to keep the plugin's order away from that query.
Database versionOption wooexp_reorder_db_version2Written by the plugin. When it differs from the plugin's table version, the table is created or upgraded at the next admin page load. Do not change it by hand.

7. Developer reference

Actions and filters the plugin fires

HookType and parametersWhen
wooexp_reorder_savedAction. $cat_id (int, product category term ID), $ids (int[], product IDs in the new order; empty after a reset)After an order was saved, copied to a translation, or reset. Fired once per category changed.
wooexp_reorder_max_productsFilter. Int, default 20000On each save request, to find the largest list accepted.
wooexp_reorder_lock_timeoutFilter. Int seconds, default 15When a save asks the database for the category lock.
wooexp_reorder_query_term_idFilter. $term_id (int), $q (WP_Query). Return a product category term ID, or 0 for none.For each query that passed the first five checks in How the order is applied on the shop, before the plugin looks for a saved order.

Example: use the order of one fixed category for a custom product query that has no category in it, and skip another one:

add_filter( 'wooexp_reorder_query_term_id', function ( $term_id, $q ) {
	if ( $q->get( 'my_featured_list' ) ) {
		return 123; // Product category term ID whose saved order should be used.
	}
	return $term_id;
}, 10, 2 );

The filter runs only after the first five checks in How the order is applied on the shop (not an admin page, not a search, no wooexp_skip, default sorting, a product query). Queries that fail one of them never reach it. It also runs when the query has no single category (the value is then 0), so it can supply one. The plugin still needs a saved order for the category you return.

AJAX actions

All five actions are for logged-in users (wp_ajax_ only), post to admin-ajax.php and need two things: the POST field wooexp_reorder holding the nonce for the action wooexp_reorder, and the capability manage_woocommerce. Otherwise the answer is HTTP 403 with "You are not allowed to do this.". Successful answers use wp_send_json_success (HTTP 200), except wooexp_reorder, which answers with a plain JSON object (no success or data wrapper; the res field tells you whether it worked).

ActionParametersWhat it does
wooexp_reordercategory (term ID)Returns the products of a category in screen order: res, link, custom, ids (all product IDs), rows (HTML of the first 40), translations (Polylang), sort_default. Answers with res false and a message when no category is chosen or the category has no products.
wooexp_reorder_rowsids_csv (comma-separated product IDs)Returns the row HTML of up to 50 of those products (post type product only).
wooexp_reorder_sortcat_id, ids_csvSaves the order. Errors: "Please select a category." (an ordinary JSON error answer, HTTP 200), 400 "None of the products belongs to this category.", 413 too many products, 409 lock busy, 500 "The order could not be saved.". The older payloads (ids[] array, or a serialised sort string with reorder-id[]) are still accepted. Success returns msg and count.
wooexp_reorder_resetcat_idDeletes the saved order of the category. Success message "Custom order removed.".
wooexp_reorder_copycat_idCopies the order to the Polylang translations of the category. Errors (400): "There are no translations to copy the order to." and "This category has no custom order to copy.". Success returns report: a list of lang, term, count.

The table

{prefix}pro_sort, created with dbDelta() using your database's character set and collation:

ColumnTypeMeaning
idbigint(20), auto increment, primary keyRow ID.
term_idbigint(20)The product category term ID.
pro_idbigint(20)The product ID.
taxovarchar(200), default emptyAlways product_cat for rows the plugin writes.
menu_orderbigint(20)The position in that category, starting at 0.

Indexes: term_order (term_id, menu_order) and pro_id (pro_id). A product in several categories has one row per category. The plugin uses MIN(menu_order) per product when a category happens to hold two rows for the same product.

Template override

The row of the list is drawn by include/product.php through WooCommerce's wc_get_template(). To change it, copy the file to wooexperts-sorting/product.php inside your child theme (or theme). The template receives $product (a WC_Product). WooCommerce's template lookup (wc_locate_template(), checked in 11.1.2) also accepts a file named product.php at the top level of the theme, so do not keep an unrelated theme file with that name unless you mean it to replace this row. Keep the outer element <div class="wooexp-reorder-product" data-title="..." data-search="..."> with both attributes: the script reads them for the row label and for the search. data-search is the lower-case text of the name, the SKU and the ID.

Options and transients

NameKindContent
wooexp_reorder_db_versionOptionThe table version, 2.
_wooexperts-sorting_licence_keyOption (not autoloaded)The licence key, after a successful activation.
_wooexperts-sorting_key_statusOption (not autoloaded)active after activation. Set to inactive when the licence server says the licence is no longer active here. Removed on deactivation.
wpxh_licence_check_v2Site transient, 12 hoursThe cached answer of the licence server, shared by all WpExperts Hub plugins.
wpxh_licence_msg_<user id>Transient, 2 minutesThe message shown once after you use the licence screen.

The plugin sets no cron events and no post or user meta.

Core and WooCommerce hooks the plugin attaches to

HookPriorityPurpose
posts_clauses99Adds the saved order to matching product queries. The plugin's sort comes first, the query's existing ORDER BY follows.
admin_menu60Adds WooCommerce → Reorder (menu title "Reorder", page title "Reorder Products", slug wooexp-reorder, capability manage_woocommerce).
admin_init10Creates or upgrades the table when wooexp_reorder_db_version differs.
admin_enqueue_scripts999Loads the screen's style (wooexp_reorder-style) and script (wooexp_reorder-script, depending on jquery, jquery-ui-sortable and jquery-touch-punch; localised object wooexp_re) on the Reorder screen only.
woocommerce_screen_ids999Adds the Reorder screen's ID so that WooCommerce admin styles load.
before_woocommerce_init10Declares compatibility with custom_order_tables, cart_checkout_blocks and product_block_editor.
deleted_post10Removes a deleted product's rows.
delete_product_cat10Removes a deleted category's rows.
plugin_action_links_wooexperts-sorting/wooexperts-sorting.php10Adds "Reorder products" to the plugin row.
product_cat_row_actions10Adds "Reorder products" to each category row.

Capabilities

  • manage_woocommerce: the Reorder screen, the row action on categories and every AJAX action.
  • manage_options: the licence screen and its actions.
  • The plugin adds no roles and no capabilities.

Screen elements and constants

  • The category drop-down is <select name="reorder-cat" id="wooexp-reorder-cat" class="reorder-cat-select">. It is not part of a form that is submitted: the script reads it and sends AJAX requests.
  • The sortable list is the ordered list #sort-reorder-pro; each row is an li.pro-handle with a data-id attribute holding the product ID.
  • Constants the plugin defines: WOOEXP_REORDER_VERSION (1.5.0) and WOOEXP_REORDER_DB_VERSION (2). The licence client reads WPXH_LICENCE_SERVER if you define it, and has the filters wpxh_licence_server and wpxh_licence_sslverify. Certificate checks are switched off only when the licence server host ends in .local, .test or .localhost, or is localhost or 127.0.0.1.
  • The main object is $GLOBALS['wooexperts_sorting'], an instance of WooExperts_Sorting. Its public methods (save_order(), get_reordered_ids(), get_ordered_ids(), get_category_product_ids(), get_category_choices()) and the static WooExperts_Sorting::table() are not a documented interface and can change.

File layout

FilePurpose
wooexperts-sorting.phpMain class: menu, AJAX actions, saving, front-end query filter, clean-up.
include/settings.phpThe markup of the Reorder screen.
include/product.phpThe product row template (can be overridden by a theme).
include/sql.phpCreates and upgrades the table.
js/reorder.js, css/admin.cssThe screen's behaviour and styles (including a layout for narrow screens, below 783 pixels).
licence/class-wpxh-licence-client.phpThe bundled licence and update client.
uninstall.php, languages/Clean-up on delete; translation template (text domain wooexperts-sorting).

8. Privacy

What is stored

  • The table {prefix}pro_sort: category IDs, product IDs and positions. It holds no personal data and no customer data.
  • The option wooexp_reorder_db_version.
  • Licence data, if you activate a licence: the licence key and its status as options, and the cached licence-server answer as a site transient (see Options and transients).
  • The plugin sets no cookies, writes no log, uses no browser storage and sends nothing to visitors' browsers. It adds a query filter on the shop pages but no scripts or styles there. Its scripts and styles load only on the Reorder screen.
  • It does not register privacy policy text or personal data exporters or erasers.

Data sent to wpexpertshub.com

Only the bundled licence client contacts another server. It sends JSON by HTTP POST to https://wpexpertshub.com/wp-json/wphub-licence/v1/<route> (the server can be changed with the WPXH_LICENCE_SERVER constant or the wpxh_licence_server filter), with a 20 second timeout. The routes and what each one sends:

RouteWhenData sent
activateYou click Activate licence.The plugin slug (wooexperts-sorting), the licence key and your site address (site_url()).
deactivateYou click Deactivate licence.The slug, the saved key and your site address. The key is removed from the site even when the server cannot be reached.
send-keyYou use "Can't find your key? Email it to me".The slug and the email address you typed.
checkWhen WordPress refreshes its plugin update list (pre_set_site_transient_update_plugins, which includes WordPress's scheduled checks and Dashboard → Updates → Check again), and when the plugin's "View details" window is opened, unless a cached answer for the same data is younger than 12 hours (one hour after a failed request).Your site address and, for every WpExperts Hub plugin registered on the site (not only this one): the plugin slug, the licence key (empty if none) and the installed version.

WordPress adds its usual User-Agent header to every remote request: "WordPress/<version>; <site address>" (checked in WordPress 7.1.2). The plugin sends no product, order, customer or category data. What the licence server stores or logs cannot be seen from the plugin's code.

What deactivation and uninstall remove

  • Deactivation removes nothing.
  • Uninstall (deleting the plugin on the Plugins screen) drops the table {prefix}pro_sort and deletes the option wooexp_reorder_db_version. On a multisite network it does this for every site. All saved orders are lost.
  • Uninstall does not delete the licence options, the licence transients or the activation on the licence server. Deactivate the licence first.

9. Troubleshooting

ProblemLikely cause and fix
The order is not applied on my category page.Your order is used only with the default sorting. Check the store's default sorting (Customizer: WooCommerce → Product Catalog → Default product sorting; the Reorder screen shows a yellow notice when it is not "Default sorting"; WooCommerce's sort menu then has no "Default sorting" entry), that the shopper has not chosen another sort, that the page shows exactly one category (not the shop page, a tag or two categories), that the category has a saved order (the summary badge says "Custom order"), and that no page cache is serving an old copy.
There is no Reorder item under WooCommerce.WooCommerce must be active and your user needs manage_woocommerce (Administrators and Shop Managers have it). If WooCommerce is deactivated the plugin's menu has no parent to attach to.
"You are not allowed to do this."The nonce on the open screen has expired or you lost manage_woocommerce. Reload the screen and try again.
"The order could not be saved. Please reload the page and try again." with a Try again button.The request failed without a message, for example your login expired or the connection dropped. Click Try again. If it keeps failing, reload the page. The previous order stays in place until a save succeeds.
"Another save of this category is still running. Please try again."Two people (or two tabs) saved the same category at the same moment and the second waited longer than 15 seconds. Wait a moment and click Try again. The wait can be changed with wooexp_reorder_lock_timeout.
"This category has too many products to be ordered in one go."The list has more than 20,000 products. Raise the limit with the filter wooexp_reorder_max_products or split the products across sub-categories.
"None of the products belongs to this category."The IDs sent are not products of this category or its sub-categories (for example products were moved to another category in another tab). Reload the screen.
"There are no products in this category."The category has no product with status Published, Private, Draft, Pending or Scheduled. Trashed products are not listed.
A product is on the Reorder list but not on the shop.Its status is Draft, Pending, Private or Scheduled, or its catalogue visibility is Hidden or Search results only (the row shows a label). The position is saved and applies when the product becomes visible.
New products appear after the ordered ones.By design. Products without a saved position follow the ordered ones. Open the category, move the product and the new order is saved.
The search box disables dragging.By design. Clear the search to drag again, or use the move buttons.
The order shows on one language but not on its translation.Every Polylang language has its own categories and its own order. Choose the translated category in the drop-down and reorder it, or use Copy order to translations on the source category. The copy button appears only when the open category has a saved order and translations.
"Copy order to translations" saved 0 products for a language.None of the ordered products has a translation in that category. That language's existing order was left unchanged.
After a cache or CDN is cleared the order is right, before it was not.Page caches keep old category pages. Purge the category page on save with the action wooexp_reorder_saved (see Caching).
On multisite, one site may log database errors about pro_sort.That site's table is created the first time an admin screen loads on it. Open any admin page of that site once.
No updates appear, or no Licence card.Open Plugins → WpExperts Hub Licences and activate your key. Cached answers last up to 12 hours: use Dashboard → Updates → Check again after activating.

10. FAQ

Where is the order used?

On product category pages and any other product query that is limited to one product category and uses the default sorting: classic theme archives, block theme templates and Product Collection blocks that produce such a query, and the shortcodes and Store API requests described under How the order is applied on the shop.

The order is not applied on my category page.

The custom order is used only with the default sorting. If your theme or a plugin forces another default sorting, set it back to "Default sorting (custom ordering + name)". The Reorder screen warns you when your store uses another default sorting.

Can customers still sort by price or popularity?

Yes. When a shopper picks another sorting option, that sorting is used instead of your custom order.

Does it change the global product menu order?

No. Positions are stored per category in the plugin's own table, so each category can have a different order and your products are not modified.

What happens to new products?

Products without a saved position appear after the ordered ones in WooCommerce's default order until you place them.

How do I move a product with the keyboard?

Tab to the handle (the three bars) of a product and press Up or Down arrow to move it one place, Page Up or Page Down for ten places, Home for the top and End for the bottom. The change is saved a moment after you stop moving.

Can I reorder on a phone or tablet?

Yes. Drag the handle, or use the arrow buttons at the end of each row. Below 783 pixels wide the screen switches to a narrow layout.

Do two people saving at the same time break the order?

No. A category is saved by one person at a time, as a whole: the other save waits and the last one wins. A failed save keeps the previous order.

How does it work with Polylang?

Every language has its own product categories, so every language has its own order. Choose the category of a language in the drop-down (grouped by language), reorder it, and use "Copy order to translations" if the other languages should follow the same order.

Which products are shown in the list?

Published, private, draft, pending and scheduled products of the category and its sub-categories, so you can position a product before it is published. Trashed products are not listed. Visitors only see the products that are visible in the catalog.

Does it work with a caching plugin?

Saving an order refreshes WordPress's cached product queries. Page-cache plugins keep their own copies of the category pages: clear them, or hook your cache plugin to wooexp_reorder_saved.

Does the plugin work without a licence?

Yes. No code checks the licence before ordering products. The licence key is used for the update information and the update package.

How do I activate my licence and get updates?

Open Plugins → WpExperts Hub Licences, paste the key from your purchase email (also in My Account → Downloads on wpexpertshub.com) and click Activate licence. New versions then appear on the normal Dashboard → Updates screen.

What is removed when I delete the plugin?

The table {prefix}pro_sort with every saved order, and the option wooexp_reorder_db_version, on every site of a network. The licence options remain, so deactivate your licence first.

11. Changelog

1.5.0 - 06/10/2026

  • New - Reorder screen redesigned: product image, SKU, price, stock status and status badges; position numbers; buttons to move a product up, down, to the top or to the bottom; search by name, SKU or ID; a summary with the number of products and whether a custom order is saved; a layout for phones and RTL.
  • New - Keyboard support: the drag handle is a button that moves with the arrow keys, Page Up / Page Down, Home and End, with announcements for screen readers; touch drag on phones and tablets.
  • New - Polylang: categories grouped by language in the dropdown and "Copy order to translations".
  • New - "Reorder products" link in Products > Categories and on the Plugins screen; a notice when the store's default sorting is not "Default sorting".
  • New - A save that fails shows the reason and a "Try again" button; leaving the page with an unsaved change asks for confirmation.
  • Fix - On hosts with PHP's default max_input_vars (1000), a category with more products lost the order of the products after the first 1000: the list was sent as one form field per product and PHP cut it. It is now sent as a single value (up to 20000 products).
  • Fix - Saving was not atomic: the old order was deleted before the new one was inserted, so a failed insert left an empty order and two people saving at once could mix their orders. A save now takes a lock per category and runs in a transaction.
  • Fix - The product IDs of a save were not checked: pages, deleted products and products of other categories could be stored. Only products of the category (and its sub-categories) are stored, and a list without any is refused instead of wiping the order.
  • Fix - Cached product queries (persistent object cache) kept the old order after a save or a reset.
  • Fix - Polylang: the category dropdown only listed the categories of the language chosen in the admin language filter; all languages are listed now, grouped by language.
  • Fix - A large category rendered every row on the server before the screen opened and could time out. Only the first 40 rows are rendered; the rest are fetched in the background.
  • Fix - Uninstall on a multisite network only removed the table of the main site.
  • Tweak - The query that applies the order is skipped for queries that opt out with the wooexp_skip query variable; the filter wooexp_reorder_max_products sets the largest list one save accepts.

1.4.0 - 27/09/2026

  • Enhancement - Licence activation and automatic updates (Plugins → WpExperts Hub Licences).
  • Security - Removed sensitive text that was shipped inside readme.txt.
  • Security - Capability check (manage_woocommerce) and nonce validation on all AJAX actions; all SQL uses prepared statements; template output escaped.
  • Fix - The admin product list queried term_relationships with the term ID instead of the term taxonomy ID, and joined saved positions of other categories (wrong list/order, and a GROUP BY error on MySQL with ONLY_FULL_GROUP_BY).
  • Fix - The table is now created with a valid dbDelta() definition and also created/upgraded after plugin updates (not only on activation); indexes added.
  • Fix - Removed the ALTER TABLE AUTO_INCREMENT query that ran on every save.
  • Fix - Saved rows are cleaned up when a product or category is deleted; duplicate IDs are ignored.
  • Tweak - The front-end order is applied with a single JOIN instead of loading every product ID of the category into a post__in list; it now also works for block themes and category-filtered Product Collection blocks.
  • Tweak - Category dropdown shows all categories hierarchically with counts; sub-category products can be ordered; non-published products are labelled.
  • Tweak - "Order saved" feedback and a "Reset to default order" button.
  • Tweak - Declared compatibility with HPOS and the Cart/Checkout blocks; Requires Plugins header; tested with WordPress 7.1 and WooCommerce 11.1.
  • Tweak - Plugin renamed to "Category Products Reorder for WooCommerce"; text domain wooexperts-sorting.

1.3 - 23/02/2022

  • Tweak - Query Improved.

1.2 - 16/06/2020

  • Tweak - Compatibility check and fix.

1.1 - 26/03/2020

  • Tweak - Code improvement.

1.0 - 12/05/2018

  • Initial Revision.

12. Support

Email support@wpexpertshub.com. Please include:

  • Your order ID.
  • The plugin version (this page describes 1.5.0), your WordPress, WooCommerce and PHP versions, and your Polylang version if you use it.
  • The category, the theme (classic or block), and the address of a category page where the order is wrong.
  • What you already tried, and the exact message you see.

More plugins and documentation are at wpexpertshub.com.