Dev.to WebDev 🛠 Dev 👁 0 📖 4 min read

WooCommerce won't filter by two attributes at once. A client needed 40 category pages that do exactly that.

This week's job: an ecommerce store selling made-to-size products wanted 40 SEO landing pages - one per popular size. Think "12 x 24 Widget Frames" as a page that ranks, instead of a filter URL that never will. The catc

This week's job: an ecommerce store selling made-to-size products wanted 40 SEO landing pages - one per popular size. Think "12 x 24 Widget Frames" as a page that ranks, instead of a filter URL that never will.

The catch is small but real, and if you've tried this you've hit it: WooCommerce's product listing tools take one attribute filter, not two.

The problem, concretely

The store models size as two product attributes: pa_width and pa_height. Their menu links to filtered listings like:

/product-category/frames/?filter-width=341&filter-height=321

Two problems with parameter URLs like that:

  1. They don't rank. Google mostly treats them as duplicates of the base category. Forty of these links means forty pages of crawl noise and zero landing pages.
  2. You can't replace them with the stock shortcode. [products attribute="width" terms="341"] works - but there's no second attribute slot. Width AND height together isn't a thing.

The site had ~350 pages, Elementor everywhere, and a live store I wasn't allowed to break (their words: "please keep in mind that this is a live site"). So: no new plugins, no theme surgery, nothing clever. Small, boring, reversible.

The fix: 30 lines and one filter hook

WooCommerce's [products] shortcode builds a WP_Query and - helpfully - passes the query args through a filter before running it. So you wrap it: accept two term IDs, inject both as a tax_query, render, clean up.

// [size_products width="341" height="321"]
add_shortcode( 'size_products', function( $atts ) {
    $atts = shortcode_atts( array(
        'width'   => '',
        'height'  => '',
        'columns' => '4',
        'limit'   => '-1',
    ), $atts, 'size_products' );

    if ( ! $atts['width'] || ! $atts['height'] || ! class_exists( 'WC_Shortcode_Products' ) ) {
        return '';
    }

    $filter = function( $query_args ) use ( $atts ) {
        $query_args['tax_query'][] = array(
            'taxonomy' => 'pa_width',
            'field'    => 'term_id',
            'terms'    => array( (int) $atts['width'] ),
        );
        $query_args['tax_query'][] = array(
            'taxonomy' => 'pa_height',
            'field'    => 'term_id',
            'terms'    => array( (int) $atts['height'] ),
        );
        return $query_args;
    };

    add_filter( 'woocommerce_shortcode_products_query', $filter, 20 );
    $shortcode = new WC_Shortcode_Products( array(
        'columns' => $atts['columns'],
        'limit'   => $atts['limit'],
        'orderby' => 'menu_order title',
        'order'   => 'ASC',
    ), 'products' );
    $html = $shortcode->get_content();
    remove_filter( 'woocommerce_shortcode_products_query', $filter, 20 );

    return $html;
} );

Notes that matter:

  • It renders your theme's normal product cards. Because it goes through WC_Shortcode_Products, the output is the standard WooCommerce loop - same markup your theme already styles. The pages look native because they are.
  • Caching is free. The shortcode caches per-args, and since width/height are part of the args, every size page gets its own cache entry. No stampede.
  • Add and remove the filter around a single render. Leave it attached and you'll quietly contaminate every other [products] call on the page.
  • Drop it in via your snippets plugin or child theme. On a live site I prefer a snippets plugin: one toggle to revert.

The part that actually took thinking: don't recategorize 350 products

The obvious alternative was "just create a size category and assign products to it." That means editing every product, duplicating information the attributes already hold, and a second source of truth that drifts. The attribute-based query means product data stays exactly as the store manages it today - new products with the right width/height attributes appear on the right size page automatically.

One prep step made the rollout mechanical: before building anything, I extracted the full attribute term map from the site's own menu markup (15 widths x 15 heights, each label already paired with its term ID in the filter URLs). Forty pages then reduced to a lookup table: page title, slug, two term IDs, done.

The SEO wrapper around it

Each size page is a normal page (cloned from the client's existing landing-page template, so design review took minutes, not days):

  • H1 and title tag with the size phrase people actually search
  • a unique intro paragraph and meta description per size
  • clean slug like /12-x-24-widget-frames/
  • the shortcode with that size's two term IDs

The old parameter URLs keep working - nothing redirects, nothing breaks. The menu just points at real pages now.

One honest footnote

The grid shows exactly what's in the catalog - which is how we found that one size was missing a variant everyone assumed existed (its mirrored orientation existed; the portrait one didn't). A faithful query is also a free data audit. Report those findings to the store owner instead of papering over them; it's the most trust you can buy with one message.

I measure before I touch anything - it's how I work through Upwork and why my write-ups have numbers in them. Previous entries in this series: cutting a WooCommerce checkout by 60% by removing an optimization plugin and how to tell if your WordPress host is the real bottleneck.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.