How to Create a Headless E-Commerce Search Experience With WP Engine’s Smart Search AI and Nuxt.js
Francis Agulto
·
Have you ever tried to buy something on a website only to have its poor search feature send you somewhere else? My stoke for finding the perfect rock climbing, coding, or running gear definitely drops when I can’t easily find what I’m looking for.
The search feature is an essential tool on any e-commerce site for converting visitors into customers. It helps users find and purchase products quickly and efficiently.
This is where WP Engine’s Smart Search AIsteps in. It’s a product for WP Engine customers that replaces WordPress’s built-in search with an intelligent, AI-driven engine for both traditional and headless WordPress applications. Smart Search AI guides visitors to the most relevant content using semantic understanding to surface better results, even for custom post types.
In this step-by-step guide, I will show you how to create a full headless WordPress e-commerce search experience with WooCommerce, WPGraphQL, and WP Engine Smart Search AI. By the end of this article, you will have created a starter e-commerce site with search functionality from start to finish.
If you prefer the video version of this article, you can access it here:
To benefit from this article, you should be familiar with the basics of working with the command line, headless WordPress development, Nuxt.js, and the WP Engine User Portal.
Steps For Setting Up:
1. Set up an account on WP Engine and get a WordPress install running. Log in to your WP Admin. Alternatively, if you are not an existing customer of WP Engine, you can get a free headless platform sandbox account here to give it a try.
2. Once in WP Admin, go to Plugins in the left sidebar, click the Add New button, search for the WooCommerce* plugin, and install it. Follow the same process to install the WPGraphQL plugin. Once both plugins are installed, activate them.
Note: Don’t forget to save your WPGraphQL endpoint, which you can access on the WPGraphQL settings page:
3. Next, go to the WooGraphQL releases page on the GitHub repo and download the latest version. Once you download the latest version, go back to your WP Admin and upload it to the plugins page.
4. We’ll run a test to ensure that WooCommerce data can be accessed via GraphQL next. First, we need to add product data to our WooCommerce store. In the left sidebar, go to Products > Add New Product:
Once you click on that, it will take you to a general Products page that shows all your products. At the top of the page, you will have the option to Add New Product, Import, or Export. This is where you can add, edit, and import products. Click on Import:
This is where you can add all my dummy product data by going to this .csv file in my repo, downloading it, then uploading it into your WooCommerce Import Products page here:
For this example, I added a product name, product description, regular price, SKU, product tag, product category, and product image.
5. Add a Smart Search license. Refer to the docs here to add a license. Contact our sales department for a free trial demo.
6. In the WP Admin, go to WP Engine Smart Search > Settings. You will find your Smart Search URL and access token here. Copy and save it. We will need it for our environment variables for the frontend. You should see this page:
7. Next, navigate to Configuration, select the Semantic card, and add the post_content, post_title, and post_excerpt fields in the Semantic settings section. We are going to use these fields as our AI-powered field for similarity searches. Make sure to hit Save Configuration afterward.
8. After saving the configuration, head on over to the Index data page, then click “Index Now”. It will give you this success message once completed :
9. Now that we have indexed our data into Smart Search, let’s make sure it works. Head over the the GraphQL IDE in your WP Admin. You can either access this via the left sidebar or the menu bar at the top of the page. Copy and paste the query below into the IDE:
This is a simple query that is asking for the first 10 products. It should give you the name, description, and image data of your products. Hit play, and you should get the results back:
Stoked!!! It works!
10. We need to set the frontend up now. The Nuxt.js frontend boilerplate will contain a project that already renders a home page with products and links to those product details pages. Clone down the Nuxt repo starting point by copying and pasting this command in your terminal
Once you clone it down, navigate into the directory and install the project dependencies:
cd my-project
npm install
11. Create a .env.local file inside the root of the Nuxt project. Open that file and paste in these environment variables (The environment variables are the ones you saved from steps 2 and 6) :
We are done with the setup steps to create the boilerplate starting point. In your terminal, run npm run dev and visit http://localhost:3000 to make sure it works. You should see this:
And when you navigate to a product detail page by clicking on a details link, you should see the detail page:
Wrap The Smart Search Endpoint
The first thing we need to do is wrap our GraphQL fetch logic—both against the WP Engine Smart Search endpoint and our WordPress GraphQL API—into a single reusable function. Create a folder called composables at the root. In that folder, create a file called useSmartSearch.js and paste in the code below:
This composable wraps all interactions with WP Engine’s Smart Search and your WordPress GraphQL endpoint.
It reads URLs and tokens from Nuxt’s runtime config, provides a private _post helper for sending GraphQL requests via $fetch, and exposes three methods: getContext for server-side semantic similarity searches, searchProducts for both AI-driven semantic queries and strict filtering of products (by toggling strictMode or supplying a custom filter string), and getProductDetails to fetch full product data—including images and pricing—directly from WPGraphQL.
Create Search Logic
The next piece brings together our Smart Search and WPGraphQL calls into a single logic layer. Create a file at composables/useSearchLogic.js and paste in the code below:
This code block glues together two back-end services: WP Engine Smart Search for all your full-text, semantic, and strict “find” queries (including category and range filters) and WPGraphQL for authoritative product details.
Each of its methods (performSearch, performActivitySearch, performPriceOnlySearch, and performCombinedSearch) constructs a single GraphQL find call that tells Smart Search exactly how to filter (via its query, filter, semanticSearch, or strictMode inputs).
Smart Search returns only the IDs, scores, and minimal data you need; then fetchCompleteProductData issues one batched WPGraphQL request to pull down images, prices, and slugs, merging them back into your UI payload.
Here’s a detailed breakdown:
Initialization
It pulls in two low-level operations from useSmartSearch:
searchProducts(query, options) to run any “find” query against the Smart Search API.
getProductDetails(ids) to fetch full WPGraphQL product data (images, pricing) by database ID.
It also defines a reactive resultsLimit (default 20) to control page size.
Mapping Basic Results
mapBasicResults takes the raw documents array returned by Smart Search—which each contains a data map and a relevance score—and converts it to a minimal product stub { id, title, description, score, image: "", price: 0 }.
Text‐Query Search (performSearch)
Validates the input query, records start time, then calls searchProducts(query, { limit }).
Throws if the API response is malformed.
Builds basic stubs via mapBasicResults, then immediately calls fetchCompleteProductData to enrich each result with image URLs and numeric pricing.
Ensures a non-empty activity value, then issues a searchProducts request where the query is simply the exact category name (e.g. “Running”) and strictMode: true to disable semantic fuzziness.
Enriches with full product data, then optionally applies a client-side price filter if one was passed in.
Price‐Only Search (performPriceOnlySearch)
Builds a “catch-all” query (“*” or scoped to a category) with strictMode: true.
Fetches the matching products, enriches them, then filters the enriched list on the client by the given { min, max } range.
Data Enrichment (fetchCompleteProductData)
Given an array of basic stubs, batches a WPGraphQL call for all their IDs.
Maps the GraphQL response nodes back onto your stubs, filling in image, price, and formatting into formattedPrice, flagging missing/failed items.
Utility and Labels
getActivityLabel maps your UI’s “activity” values (e.g., “rock-climbing”) to the exact category names in your Smart Search index.
A tiny wrapper, performCombinedSearch simply delegates to performActivitySearchso you can hook into a unified API.
Our Components
It is time to build all the components that will allow our e-commerce site to have a user experience with data rendered on the browser. At the root of the project, create a folder called components. We will be staying in the components folder for all of this section.
The Input Field
First, let’s make the Input field for our users to type into for searching.
Create a file at components/SearchInput.vue and paste in the code below:
The SearchInput.vue component renders a styled text input with a built‑in search icon and “clear” button. It accepts two props—initialQuery to seed the field and placeholder for the hint text—and binds its value to a reactive searchQuery via v‑model.
As the user types, it debounces input by 300 ms before emitting an “input” event, fires a “search” event on Enter, and shows a clear button that resets the field and emits “clear”. It also watches initialQuery for external changes and exposes clearQuery and setQuery methods so parent components can programmatically control the input.
Activity Filter
Next, let’s make the filter to allow users to select an activity. Create a file at components/ActivityFilter.vue and paste in the code below:
The ActivityFilter.vue component renders a set of pill‑style buttons—“Coding,” “Running,” and “Rock Climbing”—allowing users to select one activity at a time. It accepts an initialActivity prop to pre‑select a button and emits activity‑selected with the chosen value whenever a button is clicked.
A “Clear Filter” button appears when a selection exists, resetting the state and emitting activity‑cleared. We use Vue’s ref for reactive state, simple methods to update and clear the selection, and defineExpose to let parent components programmatically set or clear the filter.
Price Range
Now, let’s give users the ability to slide a range within pricing. Create a file at components/PriceFilter.vue and paste this code block in:
The PriceFilter.vue component provides a dual‑thumb price slider with live updates, preset buttons, and clear/apply controls. It accepts initialMin, initialMax, and maxPrice props to initialize its reactive priceRange and dynamically computes the filled‑track positions (percentLeft and percentWidth).
As the user drags either thumb, handlePriceChange clamps the values so min ≤ max, emits a price‑changed event immediately, then debounces a price‑applied event by 1 second of inactivity. Clicking a preset button jumps to that range and fires price‑applied at once, while the “Reset Price” and “Apply Price Filter” buttons emit price‑cleared and price‑applied.
It cleans up its debounce timer on unmount and exposes clearPrice and setPrice methods so parent components can programmatically reset or set the range.
Search Results Display
The next thing we need to do is display the search results. Create a file at components/SearchResults.vue and paste this code block in:
The SearchResults.vue component handles all the display states for your product search: it shows a spinning loader when isLoading is true; once results arrive (results.length > 0), it renders a header with the total count and search time alongside a “Clear Results” button, then lays out each product via a <ProductCard> grid; if the search has been performed but yielded no hits, it displays a “No products found” message; and if an error string is present, it surfaces a styled error banner with the message.
By accepting props for results, totalResults, isLoading, hasSearched, error, and searchTime, and emitting a single clear-results event, it holds all the UI you need to reflect loading, success, empty, and error conditions.
Search Bar
The next thing we need to make is the search bar, which will bring in the previous four components we just built. Create a file at components/SearchBar.vue and paste this code block in:
This is our top‑level orchestration component that stitches together four child pieces—SearchInput, ActivityFilter, PriceFilter, and SearchResults—with the shared useSearchLogic composable.
It maintains reactive state for query text, selected activity, price range, loading status, results, errors, and timing; wires each child’s events into handlers that call performSearch, performActivitySearch, or executePriceFilter; and emits high‑level lifecycle events (search-start, search-results, search-complete) for parent components.
It also listens for a global clear-search-from-home browser event to reset all filters and results, ensuring the entire search UI can be programmatically cleared from elsewhere in the app.
Add the Search Bar To All Routes
The next step is to make our search bar accessible on all product routes. To do that, we can add it to the product layout. Navigate to the layouts/products.vue file and paste this code in:
<template><div><headerclass="shadow-sm bg-white"><navclass="container mx-auto p-4"><NuxtLinkto="/"class="font-bold">Nuxt Headless WP Demo</NuxtLink></nav></header><!-- Search Bar Section --><divclass="bg-gray-50 border-b"><divclass="container mx-auto p-4"><SearchBar @search-results="handleSearchResults" /></div></div><divclass="container mx-auto p-4"><slot /></div><footerclass="container mx-auto p-4 flex justify-between border-t-2"><ulclass="flex gap-4"></ul></footer></div></template><scriptsetup>import SearchBar from"~/components/SearchBar.vue";// Handle search results from SearchBar and emit to pagesconst handleSearchResults = (searchData) => {// Dispatch custom event that pages can listen toif (process.client) {const event = new CustomEvent("layout-search-results", {detail: searchData, });window.dispatchEvent(event); }};</script><stylescoped>.router-link-exact-active {color: #12b488;}</style>Code language:HTML, XML(xml)
This updated layout file allows us to have our Search Bar on all product routes.
SVG Icons
Next, let’s make seven icon components to extract the inline SVG elements into separate Vue components to have good code readability and keep it neat.
All seven components are simple, reusable Vue components that render styled SVG icons for the different icons we need. Create a icons folder in the components directory. In that icons folder, create the files below. I linked each component to the code block you need to copy and paste into that file in my final GitHub repo for this article. Go ahead and click each file to get the code you need to paste into your own project.
Note: Update the pages/index.vue component with this code here. This imports the SVG components that the index page needs as well as handling the state.
Update The Index Page To Handle State
Lastly, let’s update our index.vue file so that the index page can handle search state. Go to pages/index.vue and paste this code in:
<template><div><!-- Loading State --><divv-if="pending"class="text-center py-12"><divclass="inline-flex items-center"><LoadingSpinnercustomClass="animate-spin -ml-1 mr-3 h-8 w-8 text-blue-500" /><spanclass="text-lg">Loading products...</span></div></div><!-- Error State --><divv-else-if="error"class="text-center py-12"><divclass="text-red-600"><ErrorIconcustomClass="mx-auto h-16 w-16 text-red-400 mb-4" /><h3class="text-xl font-medium text-gray-900 mb-2"> Failed to load products
</h3><pclass="text-gray-600 mb-4"> {{ error.message || "Please try again later" }}
</p><button @click="refresh()"class="btn">Try Again</button></div></div><!-- Default Products (shown when no search active) --><divv-else-if="!searchActive && products?.length"class="default-products"><h2class="text-2xl font-bold mb-6">All Products</h2><divclass="grid grid-cols-4 gap-5"><divv-for="p in products":key="p.id"><ProductCard:product="p" /></div></div></div><!-- No Products State --><divv-else-if="!searchActive && !products?.length"class="text-center py-12" ><divclass="text-gray-500"><EmptyBoxIcon /><h3class="text-xl font-medium text-gray-900 mb-2"> No products available
</h3><pclass="text-gray-600">Check back later for new products</p></div></div></div></template><scriptsetup>import { ref, onMounted, onUnmounted } from"vue";import ProductCard from"~/components/ProductCard.vue";import LoadingSpinner from"~/components/icons/LoadingSpinner.vue";import ErrorIcon from"~/components/icons/ErrorIcon.vue";import EmptyBoxIcon from"~/components/icons/EmptyBoxIcon.vue";// Search stateconst searchActive = ref(false);// Handle search results from layout SearchBarconst handleSearchResults = (event) => {const searchData = event.detail; searchActive.value = searchData.results.length > 0 || searchData.query.trim();};// Handle home link click to reset searchconst handleResetSearch = () => { searchActive.value = false;// Also clear the search in the SearchBar componentconst searchBarEvent = new CustomEvent("clear-search-from-home");window.dispatchEvent(searchBarEvent);};// Listen for search results from layout and reset search eventonMounted(() => {if (process.client) {window.addEventListener("layout-search-results", handleSearchResults);window.addEventListener("reset-search", handleResetSearch); }});onUnmounted(() => {if (process.client) {window.removeEventListener("layout-search-results", handleSearchResults);window.removeEventListener("reset-search", handleResetSearch); }});// Fetch the products from WooCommerce via GraphQLconst {data: products, pending, error, refresh,} = await useFetch(useRuntimeConfig().public.wordpressUrl, {method: "POST",headers: {"Content-Type": "application/json", },body: {query: ` query GetProducts($first: Int = 10) { products(first: $first) { edges { node { databaseId name image { sourceUrl altText } } } } } `,variables: {first: 10, }, },transform: (data) => {return data.data.products.edges.map((edge) => ({id: edge.node.databaseId,title: edge.node.name,image: edge.node.image?.sourceUrl || "/placeholder.jpg", })); },key: "products-list",});definePageMeta({layout: "products",});useHead({title: "Nuxt headlesswp eCommerce | All Products",meta: [ {name: "description",content:"Browse our complete collection of products in our headless WordPress eCommerce store", }, ],});</script>Code language:HTML, XML(xml)
Here is what we added to the index.vue file and what it does:
searchActive ref: Tracks whether a search is in effect so you can suppress the default “All Products” grid when search results exist.
Event handlers (handleSearchResults, handleResetSearch): Listen for custom events emitted by the shared SearchBar in the layout, updating searchActive (and clearing the bar) when searches start or are reset.
Lifecycle hooks: Hook into onMounted/onUnmounted to register and clean up those global event listeners.
pending, error, refresh from useFetch: Expose loading/error UI states and a manual retry button.
Expanded template logic: Four mutually exclusive branches to render “loading,” “error,” “default products,” or “no products” based on fetch status and search activity.
We are now ready to test this in the browser. Run npm run dev in your terminal. When visiting http://localhost:3000, you should now see a search bar and filters on your home page. Test the search and try the filters. This is the experience you should get:
We hope this article helped you understand how to create a filtered product experience in Nuxt.js with WP Engine Smart Search AI. By surfacing relevant products faster—with semantic, activity, and price-aware filtering—you give customers the ability to zero in on what they want, spend less time searching, and thus have a seamless purchasing experience.
If you’re building headless commerce, this kind of search-driven discovery can stoke engagement and revenue. We’d love to hear what you build next—drop into the Headless WordPress Discord and share your projects or feedback. Happy Coding!
* WP Engine is a proud member and supporter of the community of WordPress® users. The WordPress® trademarks are the intellectual property of the WordPress Foundation, and the Woo® and WooCommerce® trademarks are the intellectual property of WooCommerce, Inc. Uses of the WordPress®, Woo®, and WooCommerce® names in this website are for identification purposes only and do not imply an endorsement by WordPress Foundation or WooCommerce, Inc. WP Engine is not endorsed or owned by, or affiliated with, the WordPress Foundation or WooCommerce, Inc.
Get the latest headless and modern WordPress tutorials in your inbox.
Fran Agulto is a Developer Advocate at WP Engine. He is a lover of all things headless WordPress, Rock Climbing, and overall being stoked for people that love what they do and share that stoke with others! Follow me on Twitter for cool stoked headless WP!