WooCommerce Catalog SEO
Keeps WooCommerce layered-navigation filter URLs out of the search index and serves a product category sitemap straight from the taxonomy
by Alex Mochulskyi · github.com/alexskybrain/woocommerce-catalog-seo
Install
No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:
wp plugin install https://github.com/alexskybrain/woocommerce-catalog-seo/archive/refs/heads/main.zipLayered navigation is the fastest way to turn one product category into several hundred URLs that all show roughly the same products. Search engines index them, split the ranking signals between them, and the category page that should rank ends up competing with its own filters.
This plugin fixes that from four directions at once. It was written for a WooCommerce shop running YITH WooCommerce Ajax Product Filter, where filtered URLs had crowded the real category pages out of the index.
Requirements
- WordPress 6.4+
- WooCommerce
- PHP 8.2+
- Yoast SEO for the canonical and Open Graph integration. Everything else works without it.
Installation
Drop the folder into wp-content/plugins/ and activate it, or into wp-content/mu-plugins/ if it should not be deactivatable. There is no settings screen; the behaviour is controlled by filters.
What it does
Filtered views are marked noindex. A request counts as filtered when it carries yith_wcan, any filter_ or query_type_ parameter, or a product_cat query argument that did not resolve to a real category archive. Directives are written through Yoast's string and array formats, through the core wp_robots array, and as a plain meta tag as a fallback, because which of the three applies depends on what the site runs.
Category pages are self-canonical. Yoast exposes the canonical in four places that can disagree: the canonical filter, the Open Graph URL, the presentation object and the WebPage schema piece. All four are set from a single source, so a filtered view never advertises itself as the canonical page.
Single-term filters are collapsed with a 301. ?yith_wcan=1&product_cat=winter-jackets and /product-category/winter-jackets/ are one page for a visitor and two for a crawler. The filter widget is taught to link to the pretty URL in the first place, and the redirect stays as a safety net for links that already exist.
The redirect is deliberately conservative: it fires only when exactly one category is selected and no other filter is active. Multi-select and mixed filters are left alone, because folding them into a single URL would lose state.
A product category sitemap is served directly from the taxonomy. Yoast builds sitemaps from its indexables table, which drifts out of sync on a large shop after imports and bulk edits — deleted categories linger, new ones never appear. Reading terms at request time removes that failure mode. The response is cached for an hour and flushed whenever a product category is created, edited or deleted.
Filters
| Filter | Default | Purpose |
|---|---|---|
woo_catalog_seo_excluded_slugs |
['uncategorized'] |
Category slugs kept out of the sitemap. Localised shops add their own translated slug here. |
woo_catalog_seo_sitemap_stylesheet |
Yoast's XSL when installed | Stylesheet applied to the sitemap, or null for raw XML. |
Architecture
src/
Request.php is this a category, or a filtered view of one?
Robots.php noindex across Yoast, core and a meta fallback
Canonical.php canonical, Open Graph, presentation and schema
Redirects.php 301 single-term filters, pretty filter links
Sitemap.php /product_cat-sitemap.xml, cached and self-invalidating
Request holds every decision about what the current URL is, and the other four classes ask it. Keeping that judgement in one place is the point: the original version of this code repeated the same query-string inspection in five functions, and they had already started to disagree with each other.
Development
composer install
composer lint
License
GPL-2.0-or-later — see LICENSE.