Product filtering in Shopify without apps

Shopify's great, but it lacks an out-of-the-box solution to filtering products. This leads many small businesses to buy or licence third-party apps. But what if we could build one from scratch without any third party code?

If you log into your admin interface in Shopify, then navigate to your products page, you can access this neato filter for them, by clicking on the More filters button:

Showing product filtering options Shopify

This looks like the kind of thing which would be useful to your customers, right? I have bad news: There's nothing built into the front end of Shopify which allows your customers to do this. If you need this sort of functionality, up until now your only recourse was to pay for a third-party app and add it to your shop.

However, as the headline for this article has already revealed, I'm going to show you how to do this and all it will cost you is some of your time. This code is part of Finetune Partner's Mannequin library on GitHub, which offers a number of enhancements to Shopify's Theme Kit. Here's a quick run-down of the product filter features.

Allows customers to filter by:

  • Minimum price
  • Maximum price
  • Product type
  • Manufacturer
  • Product tag
  • Product in stock or not
  • Any number of arbitrary filters which can be added via metafields (instructions below)

It also has some other features:

  • Will switch to legacy paginated mode, if there are too many products to filter (the point where this happens can be configured)
  • Hides filters if there are one or less choices for the customer to make
  • Automatically adjusts the maximum and minimum price, to fit the selection of products displayed
  • Near instant results

Your latest collection

By default, your catalogue and your collections pages share the same page template, collection.liquid. The out-of-the-box solution allows you to choose how many of your products appear per page, but nothing else.

Mannequin's product filtering still includes this approach, within a noscript tag. It's also got some added Microdata.

View collection.liquid on GitHub

This is a server-side solution which allows your customers to click through page after page of your products until they buy something, or get distracted by a notification on their phone and forget you exist.

What does not seem to be possible, however, is to pass various criteria to the server and receive back a JSON snippet which just has those products in it.

So let's do the next best thing: get back all the product data and then manipulate that locally, to do the same job.

Get your product data

Shopify's liquid tags and filters mean it's trivially easy to output all of your product data onto your page. You just do this:

<script>var allMyProductData = {{ products | json }};</script>

This will give you a big old JSON file, a single entry of which looks something like this:

[
  "available": false,
  "​​compare_at_price": "5300",
  "​​compare_at_price_max": "5300",
  "​​compare_at_price_min": "5300",
  "​​compare_at_price_varies": false,
  "​​content": "Perfect for your boss",
  "​​created_at": "2020-11-27T04:39:43-05:00",
  "​​description": "Perfect for your boss",
  "​​featured_image": "//cdn.shopify.com/s/files/1/gift-basket7.png",
  "​​handle": "gift-basket-7",
  "​​id": 6012345678912,
  "​​images": "Array [ '//cdn.shopify.com/s/files/1/gift-basket7.png' ]",
  "​​media": "Array [ {…} ]",
  "​​options": "Array [ 'Title' ]",
  "​​price": 3300,
  "​​price_max": 3300,
  "​​price_min": 3300,
  "​​price_varies": false,
  "​​published_at": "2020-11-27T04:39:43-05:00",
  "​​tags": "Array(8) [ 'Basket', 'Boss', 'Bundle', … ]",
  "​​title": "Boss gift basket",
  "​​type": "basket",
  "​​variants": "Array [ {…} ]",
  "​​vendor": "Blue Sun"
]

Please do not do this, for three reasons:

  1. Performance: let's send as little data to the browser as we can get away with
  2. Security: your competitors might be sniffing about in your markup. Let's not hand them any information which we don't need.
  3. Flexibility: we'll need to add more nodes to this (spoilers)

Build your own JSON

At the bottom of collection.liquid is a script tag which, using Liquid markup, generates an Object which represents all of your products.

This code looks pretty manageable, right? But what if you have 10,000 products in your shop? All of this JavaScript is going to be written directly into the markup of the page - there's no clever fetch stuff going on here, it's all the JSON or nothing.

In the past, I've written an icon search which looped through 1,400 JSON items in a static file. The performance was fine. But the top of collection.liquid allows you to define exactly what the cut-off point is, for the number of products you're happy to search, client-side:

{% assign product_limit = 1500 %}
{% if collection.products.size < product_limit %}
{% assign load_products = true %}
{% endif %}


(I need the second variable because the value of collection.products.size changes, once it's inside a paginate tag).

Expanding the JSON

The custom JSON has a number of nodes not found in the original product data:

  • SuitableFor
  • occasion
  • mp3
  • tog
  • material

(None of these custom filters are required and if you don't find them useful, feel free to remove them)

These filters are all based on custom namespaces used in metafields. Each of these are used to create custom filters, which dynamically appear against only the collections which need them. The process of adding a new filter is a little involved, so let's deal with that first.

1. Define your metafields

Using your page based on the page.metafields template, create a new namespace for your filter. For example, the material namespace looks like this:

  • Attach metafield to: product
  • Namespace: material
  • Metafield names:
  • wool
  • wool-mix
  • cotton
  • cotton-mix
  • cashmere
  • cashmere-mix
  • angora
  • angora-mix
  • man-made
  • Data-type: Boolean (all of the above)

2. Add in your content author links (optional)

The metafield form above will generate a Shopify bulk editor link which allows logged-in Shopify content authors to change the metafield data of all of your shop's products.

This is a task which will need to be redone, as products are added and it would be laborious to configure the metafield form in exactly the correct way each time you added a product.

To save time, take a copy of the metafield bulk editor URL and add it to collection.liquid inside an if statement which ensures that only logged in content authors (and not your customers) will see it:

{% if admin == 'true' %}
<p><a href="(long URL goes here)">Edit material metafields</a></p>
{% endif %}

This functionality has a dependency on the admin-check snippet in Mannequin.

3. Content load

If you follow the Shopify bulk editor link above, you'll see something like this:

Grab of Shopifys bulk product editor

What's hopefully clear is that not all of these metafields are appropriate for all products - it's extremely unlikely that a cashmere MP3 player will be sold any time soon, for example. As the bulk editor in Shopify displays all products, your content authors should concern them with only adding the relevant data to the relevant product.

At this point, your content authors should mark the products which match your new metafields appropriately.

Side note: why boolean?

It's possible to save metafield data as boolean, strings, numbers and currency values. Unfortunately, when this data is returned by Shopify as JSON, each metafield has only two data types:

  • String
  • Integer

Booleans are converted to integers, with 0 being false and 1 being true. This means there's no way to determine if one specific value is a boolean or just a particularly low value integer. The product filter doesn't work with strings, because they're kind of meaningless to compare with one another.

Most other product filters work on this principle too - the user selects from a fixed-list of manufacturers, for example, rather than typing one in.

4. Add the new metafield namespace to your JSON

In collection.liquid, add a new node onto the end of the JSON. For example:

{% if product.metafields.material.size > 0 %}
  "material": "{% for type in product.metafields.material %}{% if type.last == 1 %}{{ type.first }}|{% endif %}{% endfor %}",
{% endif %}

You should replace material (all three of them) with whatever you called your new namespace in your custom metafields.

5. Add your new product filter

At the top of collection.liquid, add your new product filter logic to the file. Here's what the material filter looks like:

{% capture allMaterial %}{% for product in collection.products %}{% for type in product.metafields.material %}{{ type.first }}|{% endfor %}{% endfor %}{% endcapture %}
{% assign uniqueMaterial = allMaterial | split: "|" | uniq %}
{% comment %} Only show filters relevant to this collection {% endcomment %}
{% if uniqueMaterial.size > 1 %}
  <fieldset>
    <legend>Material used</legend>
      <p>
        {% for type in uniqueMaterial %}
        <input type="radio" name="material" id="{{ type }}" data-js="metafield">
        <label for="{{ type }}">{{ type }}</label>
        {% endfor %}
      </p>
  </fieldset>
{% endif %}

Note that the variables allMaterial and uniqueMaterial are unique to this filter - if you were adding a different filter, these would need to be renamed accordingly.

The name attribute of the radio button inside the for loop inside the filter must match the namespace of your new metafield.

This markup's pretty flexible, apart from the following:

  • The JavaScript makes assumptions about relationships between the label and input tags, so leave them intact, if you can
  • All the data-js attributes are used by the JavaScript to process the data in various ways

Because of those if statements, your new filter should only appear within collections where it's relevant.

Customising the filter

The markup for the filter is pretty flexible, but here are some elements to watch out for:

  • The IDs price-range-min and price-range-max (for the range elements) are hard-coded into the JavaScript
  • The inStock ID is hard-coded into the JavaScript

The product filter also has a particular way of working:

  • The product types, vendors and any custom metafield data are radio buttons: only one value is considered during any one search and this search is exclusive
  • The tags are inclusive - if the user selects two tags which are unique to two different products, both products will be shown

Dependencies

Mannequin has no dependencies outside of the repo, but the product filter involves a number of different files, some of which might not be obvious:

Please see the post on editing metafields without an app for the dependencies involved in that process.