How to Create a WooCommerce New Arrivals Category That Expires
A WooCommerce new arrivals category needs more than date sorting. Here are the three levels that work, plus the expiry rule nobody writes about.
Sorting the shop by date is not a new arrivals page. It is a sorted view of everything you have ever published. If you want a WooCommerce new arrivals category you can drop into the main menu, point a campaign at, and let Google index, you need something with its own URL and its own membership list — and, more importantly, something that takes products back out again.
There are three levels to this and they are genuinely different objects. Most write-ups describe the first one, call it solved, and never mention that the hard half is expiry.
The short answer#
A WooCommerce new arrivals category should be a real product_cat term whose membership is set by a rule — products created in the last 30 days — plus an automated job that removes each product once it ages out.
- Sorting is not a category. Linking to ?orderby=date only gives a date-sorted view of the whole shop: no unique heading or content, nothing ever expires from it, and SEO plugins canonicalise the sorted URL back to the plain archive.
- Shortcodes stop short of taxonomy. The Product Collection block or [products orderby=”date”] will produce a page you can link from a menu, but it has no breadcrumbs, product counts, category widget entry, or layered navigation and filter blocks.
- Expiry is the whole problem. A real product_cat term brings back breadcrumbs, counts, menus and filters, but it only stays accurate if a scheduled daily job adds products as they are published and removes them as they age past the window — without one, New Arrivals becomes a slowly growing copy of your catalogue.
- Pick the date field carefully. Build the rule on date created, never date modified (price edits, stock decrements and bulk updates bump it), and size the window to your publishing rate so the page holds two to four screens of products.
- Require in-stock as well. New products sell fastest, so making stock status part of the membership rule keeps sold-out novelties off the top row without hiding out-of-stock items across the whole site.
Level 1: sorting the shop by date#
WooCommerce already ships this. The catalogue sorting dropdown includes Sort by latest, which sets ?orderby=date on the current archive. It orders by the product’s publish date, descending, with the post ID as the tie-break. You can link straight to it:
https://example.com/shop/?orderby=date
https://example.com/product-category/dresses/?orderby=date
If you want every archive to open that way, set it once under Appearance → Customize → WooCommerce → Product Catalog → Default product sorting and choose Sort by latest. That writes the woocommerce_default_catalog_orderby option; no code needed. On a block theme the Customize link may be missing from the admin menu — open /wp-admin/customize.php directly and the WooCommerce panel is still there.
This is free, instant, and correct as far as it goes. What it is not is a destination:
- No content of its own. You cannot give
?orderby=datea heading, an intro paragraph, or a banner. It is the shop page wearing a different order. - It is a query string, not a page. SEO plugins generally canonicalise sorted URLs back to the unsorted archive, which is the right call — you do not want six sorted copies of the shop competing. But it means the sorted view is not a thing Google will rank.
- It never ends. Page one is new. Page nine is 2019. There is no boundary between “new arrival” and “still in the catalogue”.
- It is not filterable as a set. You cannot say “new arrivals under £50” without stacking more parameters, and you cannot exclude a product from it.
If all you need is for returning customers to spot fresh stock, stop here. It costs nothing and it works.
Level 2: the New Arrivals collection and the products shortcode#
The next step up is a display block you place on a real Page. In current WooCommerce that is the Product Collection block. Insert it, and in the block’s collection chooser pick New Arrivals. By default it shows products created in the last 7 days; that window is editable in the block sidebar, along with the number of products, the column count and an inventory-status filter.
The older standalone Newest Products block still renders wherever it is already in use, but WooCommerce 9.5 soft-deprecated the product grid blocks and hid them from the inserter, so on a new page you will not find it. Product Collection is the replacement.
If you are in the classic editor or a page builder, the shortcode does the same job:
[products limit="12" columns="4" orderby="date" order="DESC"]
WooCommerce 3.2 consolidated [recent_products], [featured_products], [sale_products] and several others into [products] with attributes. [recent_products] still resolves, but [products] is the one the documentation describes and the one that takes the full attribute set.
Now you have a URL, a title, an intro, and something you can link from the menu. What you still do not have is a category. The distinction matters more than it sounds:
- No term, so no term behaviour. No breadcrumb trail, no product count, no entry in category dropdowns or the category widget, nothing in the Products → Categories list.
- Layered navigation and filter blocks do not apply. Attribute filters operate on the product archive query. A shortcode grid inside a Page is a separate query and filters will not touch it.
- Pagination is awkward. A grid on a Page paginates independently of the Page, and themes vary in how gracefully they handle it.
- Nothing else on the site can see the set. Feed exporters, discount rules scoped to a category, related-product logic — these read taxonomy terms, and there is no term here.
Level 3: a real WooCommerce new arrivals category#
The third option is an ordinary product_cat term called New Arrivals, sitting at /product-category/new-arrivals/, with products actually assigned to it. Everything above comes back: breadcrumbs, counts, menus, filters, the sorting dropdown, and the ability to write a genuine intro on the category description.
Creating it takes ten seconds. Keeping it accurate is the whole problem. A person adding twelve products a week and remembering to tick a box will manage for about a month. What nobody manages is the removal — and a New Arrivals category that only ever gains members quietly becomes “every product we have ever stocked”, sorted badly. This is the same failure mode as a sale category nobody empties, which we covered in how to build a WooCommerce sale category that empties itself.
The half nobody covers: expiry#
Something has to run on a schedule, look at every product in the category, and remove the ones that have aged out. If you want to write it yourself, this is the honest version. Put it in a small site plugin rather than your theme’s functions.php, so it survives a theme change.
<?php
/**
* Plugin Name: New Arrivals Sweeper
* Description: Keeps a "new-arrivals" product category limited to the last 30 days.
*/
const MY_NA_SLUG = 'new-arrivals';
const MY_NA_DAYS = 30;
add_action( 'init', function () {
if ( ! wp_next_scheduled( 'my_refresh_new_arrivals' ) ) {
wp_schedule_event( time(), 'daily', 'my_refresh_new_arrivals' );
}
} );
register_deactivation_hook( __FILE__, function () {
wp_clear_scheduled_hook( 'my_refresh_new_arrivals' );
} );
add_action( 'my_refresh_new_arrivals', function () {
$term = get_term_by( 'slug', MY_NA_SLUG, 'product_cat' );
if ( ! $term ) {
return;
}
$cutoff = gmdate( 'Y-m-d H:i:s', time() - ( MY_NA_DAYS * DAY_IN_SECONDS ) );
// Products that SHOULD be in the category.
$fresh = get_posts( array(
'post_type' => 'product',
'post_status' => 'publish',
'posts_per_page' => -1,
'fields' => 'ids',
'date_query' => array(
array( 'after' => $cutoff, 'column' => 'post_date_gmt' ),
),
) );
// Products that ARE in the category right now.
$current = get_posts( array(
'post_type' => 'product',
'post_status' => 'any',
'posts_per_page' => -1,
'fields' => 'ids',
'tax_query' => array(
array(
'taxonomy' => 'product_cat',
'field' => 'term_id',
'terms' => $term->term_id,
'include_children' => false,
),
),
) );
wp_defer_term_counting( true );
foreach ( array_diff( $fresh, $current ) as $id ) {
wp_set_object_terms( $id, array( (int) $term->term_id ), 'product_cat', true );
}
foreach ( array_diff( $current, $fresh ) as $id ) {
wp_remove_object_terms( $id, array( (int) $term->term_id ), 'product_cat' );
}
wp_defer_term_counting( false );
} );
That is a working script, and for a catalogue of a few hundred products on a site with steady traffic it is fine. Know what you are accepting:
- WP-Cron needs traffic. A quiet site may not fire the daily event for days. Disable WP-Cron and call
wp-cron.phpfrom a real system cron if the timing matters. posts_per_page => -1does not scale. At a few thousand products you will want to batch this, and at that point you are writing a queue.- It cannot tell your assignments from its own. If you manually put a hero product in New Arrivals, the next sweep removes it. There is no record of who assigned what — you would need to store that yourself.
- Daily is the resolution. A product published this morning may not appear until tomorrow unless you also hook
save_post_product. - Removal can leave a product with no category. If New Arrivals was the only category on an item, it will show up uncategorised. Check that before you let the sweep run on a live catalogue.
The same shape of problem — rules that have to be re-applied, not applied once — is worth reading about in how to automatically add products to a WooCommerce category based on rules, and if you are currently doing this by hand in bulk edit, bulk assigning products to categories covers why that keeps coming back.
Which date should you actually use?#
This is where new arrivals pages go wrong quietly, and it is worth being explicit about the trade-off.
Date created#
WooCommerce’s date_created is the product post’s post_date — when the product record came into existence in WordPress. For a store where someone adds products by hand as stock arrives, that is exactly right and you should use it.
For a store that was migrated or is fed by an importer, it is not. If you moved 4,000 products in from another platform last Tuesday, all 4,000 have a creation date of last Tuesday. Your New Arrivals page is your entire catalogue for the next 30 days, and then it is empty. Worse, some import tools rewrite the post date on every sync, so products keep re-entering the window.
Three ways out, in order of how much work they are:
- Map the real date on import. Most CSV importers let you set the post date from a source column. If your old platform exported a created-at field, map it and the problem disappears permanently.
- Wait out the window. Turn the category on 30 days after the migration. Crude, free, and works if you only migrate once.
- Use your own field. Write a
_first_stockedmeta value when a product genuinely goes live and match on that instead of the post date. This is the only option that survives repeated imports, and it is the one to pick if products arrive from a supplier feed.
Not date modified#
It is tempting, because “recently updated” sounds close to “recently added”. It is not. A price change, a stock decrement, a typo fix in the description, or a bulk plugin touching meta all bump post_modified. A product from 2021 whose price you corrected this morning becomes a new arrival. Never build the rule on it.
How long is “new”?#
Pick the window from your publishing rate, not from a round number. Aim for a page that is consistently two to four screens deep. If you add ten products a week, 30 days gives you roughly 40 products — a solid page. If you add two products a month, 30 days gives you a two-product page, which is a worse experience than no page at all and a weak candidate for indexing. Stretch the window to 90 days, or do not build the page.
Stop out-of-stock novelties sitting at the top#
This is the specific failure that makes a date-based page look neglected. New products sell out fastest. A date-sorted page puts the newest thing first. So the very top of your New Arrivals page fills with the items you cannot sell, and the customer’s first impression of your freshest stock is a row of greyed-out cards.
Because it is a real category, you have a real fix: make stock part of the membership rule, not just the sort order. The rule becomes two conditions joined with AND — created in the last 30 days and stock status is In stock. An item that sells out drops out of the category; if it is restocked inside the window, it comes back. That last part is the bit hand-rolled scripts usually miss, because it means the rule has to be re-evaluated on stock change, not only on a daily timer.
The alternatives, honestly:
| Approach | Effect | Catch |
|---|---|---|
| WooCommerce → Settings → Products → Inventory → Out of stock visibility | Hides out-of-stock products from the catalogue and search | All-or-nothing across the whole site, and the product page stays reachable by direct link anyway |
| Push out-of-stock to the end of the archive | Keeps the product reachable, just last | Needs a query tweak or a plugin; see showing out-of-stock products last |
| Make stock part of the category rule | Scoped to this one category only | Requires re-evaluation on stock change, not just daily |
If your New Arrivals page is the one you link from email campaigns, the third is the right answer. Everywhere else on the store, out-of-stock products should usually stay visible.
If you would rather not maintain the script: Smart Categories#
Full disclosure, this is our plugin. If the code above does what you need, use the code above — it is free and it is yours.
Smart Categories for WooCommerce attaches a rule set to a real product_cat term, so the outcome is the same object described in Level 3 — an ordinary category with an ordinary archive, counts, breadcrumbs and menu behaviour. For this page the rule is two conditions in one ALL OF group:
- Date created — in last 30 days. Or the Custom field matcher with your own
_first_stockedkey, if the post date lies to you. - Stock status — equals In stock. The sold-out novelty leaves the page and returns when it is restocked.

The expiry is the daily sweep. Products are re-evaluated when they are created or updated and when price or stock changes, plus once a day for everything — which is what catches products crossing the 30-day line, since nothing about that product changed on the day it aged out. Matching runs in the background through Action Scheduler in batches, not on page load. Assignments you made by hand are preserved: the plugin records the memberships it created and only removes those, which is the limitation of the script above.
You can see the matching products and the count while you compose the rule, before saving, which mostly exists so you find out that your 30-day window returns four products before you publish the page rather than after.

Date created, Stock status and the Custom field matcher are all in the free version on wordpress.org, with no cap on categories, rules or products. Nothing in this article needs the paid tier.
What none of this fixes#
A new arrivals category is a merchandising page first and an SEO page second, and it is worth being clear about where the boundary sits.
- Meta titles and descriptions. No category-automation tool writes these, ours included. Yoast, Rank Math or SEOPress own that, and you should write the New Arrivals meta by hand once.
- Thin-page risk. A category whose membership rotates completely every 30 days, with a handful of products at a time, is a hard page to rank and may not get indexed at all. Why WooCommerce category pages are not indexed goes into what Google actually does with these. Treat New Arrivals as a page for people who already know your store.
- Redirects. If you are restructuring categories around this, redirects are a separate job with a separate tool — a redirect plugin or a server rule.
The short version: if you only need customers to find fresh stock, ?orderby=date is free and takes a minute. If you need a page you can link, style and send traffic to, build a real category — and decide on day one what removes products from it, because that decision is the difference between a New Arrivals page and a slowly growing copy of your catalogue.