Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.uselayers.com/llms.txt

Use this file to discover all available pages before exploring further.

Overview

This is the SDK equivalent of Implementing search in Liquid. The same four moving pieces apply — autocomplete, prepare, execute, facets/sorts — but each is a method on a controller instead of a hand-rolled fetch. The SDK handles request deduplication, the search_id lifecycle, abort signals, and reactive state for you. Use this version when you’ve already installed @commerce-blocks/sdk in your theme. For a no-dependencies version, see the Liquid + Fetch guide.

Prerequisites

  • The Layers Shopify app installed and synced.
  • The SDK installed with sorts and facets mirrored from metaobjects — see the setup snippet.
  • The Storefront Pixel installed so deviceId / sessionId are populated automatically.

Controllers used

ControllerPurpose
client.suggest()Typeahead suggestions, including semantic redirects
client.search()Prepare + execute the full search request
Both expose the same reactive state signal and subscribe() shape as the collection controller.

1. Autocomplete in the header

client.suggest() debounces input changes, aborts the previous request, and emits suggestions on its state signal. It returns the same redirect metadata as the Autocomplete API — when a typed query matches a semantic redirect, _meta.redirect.url is populated and you should navigate the shopper directly.
<form action="/search" method="get" role="search">
  <input type="search" name="q" autocomplete="off" id="search-input" />
  <ul id="search-suggestions" hidden></ul>
</form>
const input = document.getElementById('search-input')
const list = document.getElementById('search-suggestions')

const suggest = window.layers.suggest({ debounce: 200 })

suggest.subscribe(({ data }) => {
  if (!data) return

  if (data._meta?.redirect?.url) {
    window.location.href = data._meta.redirect.url
    return
  }

  list.innerHTML = data.matchedQueries
    .map(
      (s) =>
        `<li><a href="/search?q=${encodeURIComponent(s.query_text)}">${s.query_text}</a></li>`,
    )
    .join('')
  list.hidden = data.matchedQueries.length === 0
})

input.addEventListener('input', (event) => {
  const query = event.target.value.trim()
  if (!query) {
    list.hidden = true
    return
  }
  suggest.execute(query)
})
The controller handles debounce, abort, and dedup internally. No manual AbortController plumbing.

2. Prepare the search before the results page

prepare() is a first-class method on client.search(). Call it the moment a shopper commits to a search — the SDK kicks off the request, caches the resulting search_id against the query, and execute() automatically reuses it on the next page. Option A — Prepare on submit. Best when you can intercept the form submit so prepare keeps running during navigation.
const form = document.querySelector('form[role="search"]')
const search = window.layers.search()

form.addEventListener('submit', async (event) => {
  event.preventDefault()
  const query = form.q.value.trim()
  if (!query) return

  // Fire and forget — the SDK persists the search_id internally
  search.prepare({ query })

  window.location.href = `/search?q=${encodeURIComponent(query)}`
})
Option B — Prepare on the search results page. Simpler, but loses the in-flight head start.
<script>window.layersSearchQuery = {{ search.terms | json }}</script>
const search = window.layers.search()
await search.prepare({ query: window.layersSearchQuery })
Don’t await prepare() on the critical path. It returns a result the SDK can keep working on in the background while you render the next page.
On the search results page, execute() reuses the prepared search_id automatically — you don’t pass it. The SDK falls back to standard processing if the 15-minute cache has expired or no prepare ran.
const search = window.layers.search()

search.subscribe(({ data, isFetching, error }) => {
  if (isFetching) showLoading()
  if (error) showError(error.message)
  if (data) {
    renderGrid(data.products)
    renderFacets(data.facets, data.priceRange)
    renderPagination(data.page, data.totalPages)
  }
})

const query = window.layersSearchQuery
const params = new URLSearchParams(location.search)

await search.execute({
  query,
  sort: params.get('sort') || undefined,
  filters: readActiveFiltersFromUrl(params),
  page: Number(params.get('page')) || 1,
})
Controller options merge across calls, so subsequent interactions only pass what’s changing:
search.execute({ sort: 'price-asc', page: 1 }) // change sort, reset page
search.execute({ filters: { color: 'Red' } }) // add filter, keep sort
search.execute({ page: 2 }) // pagination, keep everything else

4. Render facet and sort controls from Liquid

The control surfaces are identical to the collection-page equivalents — see Rendering facets with the SDK and Rendering sort orders with the SDK. On a search page, filter sort metaobjects by the search scope:
{% assign layers_sorts = shop.metaobjects['app--278936322049--sort_order'] | sort: 'order' %}
{% assign layers_facets = shop.metaobjects['app--278936322049--facet'] %}

<section class="search-controls" data-query="{{ search.terms | escape }}">
  <select id="sort-order" name="sort">
    <option value="">Relevance</option>
    {% for sort in layers_sorts %}
      {% assign scopes = sort.scope.value %}
      {% if scopes contains 'search' %}
        <option value="{{ sort.code.value }}">{{ sort.name.value }}</option>
      {% endif %}
    {% endfor %}
  </select>

  <aside class="filters">
    {% for facet in layers_facets %}
      <section data-facet-code="{{ facet.code.value }}">
        <h3>{{ facet.name.value }}</h3>
        <div class="filter-values" data-loading="true"></div>
      </section>
    {% endfor %}
  </aside>
</section>
The end-to-end timeline:
1

Header submit

Shopper hits enter. Call search.prepare({ query }) (non-blocking) and navigate to /search?q=.... The SDK persists the resulting search_id so the next page can use it.
2

Results page render

Liquid renders the sort dropdown and empty facet groups from metaobjects, including the active sort/filter state from the URL.
3

Execute

Page JS calls search.execute({ query, sort, filters, page }). The SDK reuses the prepared search_id automatically. The subscriber renders the grid, facets, and pagination as soon as data lands.
4

Interaction

On sort change or filter toggle, call search.execute({ … }) with just the changed fields. Mirror the state to the URL via history.replaceState.

Why the SDK over Fetch

ConcernFetch versionSDK version
Debounced autocompleteDIY debounce + AbortControllerclient.suggest({ debounce })
search_id lifecyclesessionStorage plumbing across pagesHandled internally
Re-rendersDIY DOM updatesSignal subscriber
Sticky optionsTrack manuallyMerged across execute() calls
Click feedback / attributionRead attributionToken, POST to feedback endpointStorefront Pixel handles standard themes; SDK exposes attributionToken on results for custom flows
ErrorsHand try/catchStructured ClientError with isRetryable()

Troubleshooting

Suggestions show but redirects don’t fire. The redirect lives at data._meta.redirect.url on the suggest state. Make sure your subscriber checks for it before rendering the suggestion list. execute is slow on the results page. Prepare wasn’t called, or more than 15 minutes have passed. The SDK falls back to normal processing — confirm prepare runs before navigation. Filters don’t apply. Aliases must be registered in filterAliases on the client. URL-friendly keys (color) only resolve to API codes (options.color) if the alias is configured.

See also