Adding Instant Search for WooCommerce

,

In the Building E-Commerce Search article we looked on how Findkit can be used to implement the common use cases for e-commerce search like filtering based on the price and stock availability among full text searching. In the article we just used imaginary e-commerce site but lets now look at in practice how we can expose the product data from WooCommerce to the Findkit Index, how to load Findkit UI library using that data and to add items to the shopping cart directly from the search results.

Exposing Product Data from WooCommerce

The product data must be exposed using the Findkit Meta Tag but since WooCommerce is based on WordPress we can leverage the WordPress Findkit plugin which allows us to use simple WordPress filter to add additional data to the Meta Tag

add_filter('findkit_page_meta', 'findkit_woo_page_meta', 10, 2);

function findkit_woo_page_meta($meta, $post) {
	$product = wc_get_product($post->ID);

	if (!$product) {
		return $meta;
	}

	$meta['customFields']['productPrice'] = [
		'type' => 'number',
		'value' =>  $product->get_price(),
	];

   return $meta;
}

In the filter we just use the wc_get_product() function to detect WooCommerce products and add custom price field when we encounter once. We can also add description, image and product id:

$meta['customFields']['productId'] = [
    'type' => 'number',
    'value' =>  $post->ID,
];

$meta['customFields']['productDescription'] = [
	'type' => 'keyword',
	'value' =>  $product->get_short_description(),
];

$image_id  = $product->get_image_id();
$meta['customFields']['productImageURL'] = [
	'type' => 'keyword',
	'value' =>  wp_get_attachment_url($image_id),
];

See the WC_Product class api docs for details.

Testing Changes Locally

You can now check with a local test crawl that the product data is exposed correctly as Custom Fields using the Findkit CLI

$ findkit crawl test --local-http https://www.example.local/products/2

HTTP Status: 200
Crawl Status: ok
Message:
URL: https://www.example.local/products/2
Title: The Product
Language: en
Tags: wordpress, domain/www.example.local, wp_post_type/product
Created: 2022-12-01T09:37:32.000Z
Modified: 2023-11-10T11:30:48.000Z
Custom fields:
    productPrice [number]: 38
    productImageURL [keyword]: https://www.example.local/wp-content/uploads/2021/06/the-product-600x471.jpg
    productId [keyword]: 184650
    productDescription [keyword]: Short product description

If everything looks good, deploy the changes to production and run Findkit Full Crawl to expose the product data to the Index

findkit crawl start

Including the Findkit UI Library

First we need to register a custom script to WordPress. In your theme add findkit-woo/findkit-woo.js with

import { FindkitUI } from "https://cdn.www.findkit.com/ui/v0.15.0/esm/index.js";

and in the functions.php file register it

add_action('wp_enqueue_scripts', 'findkit_woo_register_script');
function findkit_woo_register_script() {
	wp_register_script('findkit-woo', get_template_directory_uri() . '/findkit-woo/findkit-woo.js', [], '1.0.0', true);
	wp_enqueue_script('findkit-woo');
}

Since FindkitUI is exposed as a module script we need to convert the script tag to use type=module attribute so we can import the Findkit UI library

add_filter('script_loader_tag', 'findkit_woo_make_module_script', 10, 2);
function findkit_woo_make_module_script($tag, $handle) {
	if ($handle === 'findkit-woo') {
		return str_replace("type='text/javascript'", "type='module'", $tag);
	}

	return $tag;
}

Alternatively you can also install the Findkit UI library from npm if you have a bundling setup in your theme.

Implementing the Search Interface

Now we can implement the search interface. We start by removing the search terms limit so the products can be browsed without entering any search terms and by limiting the results to the products with the price custom field:

const ui = new FindkitUI({
  publicToken: "<token>",
  minTerms: 0,
  params: {
    filter: {
      productPrice: { $exists: true },
    },
  },
});

// Open the search from the search button, but we could
// also embed it into a div element with the 
// `container` constructor option
ui.openFrom("button.search");

Let’s modify the Hit slot to display our custom fields by creating a custom Hit component.

import {
	FindkitUI,
	html
} from "https://cdn.www.findkit.com/ui/v0.15.0/esm/index.js";

function Hit(props) {
  const { TitleLink, Highlight } = props.parts;
  const price = props.hit.customFields.productPrice?.value;
  const description = props.hit.customFields.productDescription;
  const image = props.hit.customFields.productImageURL?.value;
  const productId = props.hit.customFields.productId?.value;
  

  return html`
    <${TitleLink} />

    ${image && html` <img class="product-image" src=${image} /> `}

    <!-- Show highlight only when the user has searched for
         something, otherwise fallback to showing the description -->
    ${props.hit.highlight
      ? html` <${Highlight} /> `
      : html` <div class="product-description">${description}</div> `}

    <div class="product-price">Price: ${price}</div>
  `;
}

and pass it to the slots in the FindkitUI constructor

const ui = new FindkitUI({
    // ...
    slots: { Hit }

Adding to Cart from the Search Results

As a bonus we can add “Add to Cart” functionality directly to the search results so users won’t even need to navigate to the product pages. To add products to the WooCommerce cart we need a helper function:

async function addToCart(productId) {
  if (typeof wc_add_to_cart_params === "undefined") {
    console.error(
      "the wc_add_to_cart_params global is undefined, cannot add to cart",
    );
    return;
  }

  const data = new FormData();
  data.append("product_id", productId);

  const res = await fetch(
    wc_add_to_cart_params.wc_ajax_url.replace("%%endpoint%%", "add_to_cart"),
    {
      method: "POST",
      body: data,
    },
  );

  if (!res.ok || res.status !== 200) {
    throw new Error("Failed to add to cart");
  }

  const resData = await res.json();

  if (resData.error) {
    throw new Error("Failed to add to cart");
  }
}

and a component which calls it on a button click:

import {
	FindkitUI,
	html,
	preact,
} from "https://cdn.www.findkit.com/ui/v0.15.0/esm/index.js";

const { useState } = preact;

function AddToCart(props) {
  const { productId } = props;
  const [status, setStatus] = useState("initial");

  const onClick = async () => {
    if (status !== "initial") {
      return;
    }

    setStatus("adding");

    try {
      await addToCart(productId);
      setStatus("added");
    } catch {
      setStatus("error");
    }
  };

  if (status === "error") {
    return html` <div class="error">Failed to add to cart</div> `;
  }

  if (status === "added") {
    return html`
      <div>
        Added to cart.
        <a href=${wc_add_to_cart_params.cart_url}>View cart.</a>
      </div>
    `;
  }

  return html`
    <button
      class="add-to-cart"
      disabled=${status !== "initial"}
      type="button"
      onClick=${onClick}
    >
      ${status === "initial" ? "Add to Cart" : "Adding..."}
    </button>
  `;
}

Add add the the AddToCart component to the Hit component

function Hit(props) {
  // ...
  const productId = props.hit.customFields.productId?.value;

  return html`
    <!-- ... -->
    <div class="product-price">Price: ${price}</div>
    <${AddToCart} productId=${productId} />
  `;
}

To put everything together with extended comments checkout this git repository:

In the next article in this series we will look how this can be implemented as a Gutenberg Block with @wordpress/scripts bundling so it can be placed visually anywhere on the site using the WordPress full site editing capabilities.