Integration Guides

Shopify Integration

Add SearchX to a Shopify store — search bar in the header, a full results page at /search, and Add to Cart — using Custom Liquid sections. No app install required.


Overview

SearchX on Shopify has two independent halves, and you need both:

  • Catalog (data). Your Shopify products have to be imported into your SearchX index. SearchX imports a product feed in Google Merchant (XML) format, so you generate that feed with a feed app and point SearchX at its URL.
  • Storefront (UI). SearchX is a JavaScript widget that mounts into <div> containers inside your theme. You add it through Custom Liquid sections — one in the header for the search bar, one on the search template for the results page.

The UI without an index shows empty results; the index without the UI is invisible. Set up the catalog first, then the storefront.

Tested theme

This guide targets Online Store 2.0 themes and is written against Shopify's Horizon theme. The approach works on Dawn-based themes too; only a few CSS selectors (header classes, the cart drawer element) differ between themes — those spots are called out below.


Requirements

  • A SearchX application with an App ID and a public API / search key (the key that ships to the browser — never a private/admin key).
  • A Shopify store with an Online Store 2.0 theme and Customize theme (or Edit code) access.
  • A feed app that produces a Google Merchant XML feed (see Step 1).

Public key only

The api_key you place in the theme is public — it is visible in the browser, which is expected for a client-side search widget. Never put a private or admin key in your theme.


At a glance

The whole integration is five steps:

  1. Build a product feed and import it into SearchX (Steps 1.1–1.3).
  2. Connect the feed and set your store's Origin URL in the SearchX dashboard (Steps 1.4–1.5).
  3. Paste the header snippet into a Custom Liquid section in your theme's Header group (Step 2).
  4. Add a results container on the Search template and hide the native search (Step 3).
  5. Test search and Add to Cart (Step 4 and the Checklist).

The only values that are truly unique to you are your App ID, your public API key, and your store's Origin URL. Everything else in the snippet works as-is — styling and language are optional. The exact placeholders to change are listed under What to replace in Step 2.


Step 1: Get your catalog into SearchX

Shopify does not expose a downloadable XML feed at a stable URL on its own, so you use a feed app to generate one.

1.1 Install a feed app

Any Google Merchant feed app works — for example Pify Google Shopping Feed, Omega, AdNabu, Simprosys, or Channable. They all follow the same idea: they build an XML file at a URL you can hand to SearchX.

1.2 Configure the feed (important)

These settings make the feed compatible with SearchX and with Add to Cart. In Pify they live under Global Settings and Create Feed → Base Settings; other apps have equivalents.

SettingValueWhy it matters
Product ID FormatVariant ID as Product IDThe most important one. Makes each item's g:id the Shopify variant id, which is what Add to Cart needs.
Variant PreferenceAll VariantsOtherwise only one size per product is exported and the rest disappear.
Unique Product IdentifiersSubmit Brand Name and MPN (SKU)Keeps the brand (for the facet) without dropping products that have no barcode. The GTIN option drops those.
Append Variant Name to TitleNoClean titles; size lives in the facet, not the title.
Use Rich Product DescriptionOffPlain text — otherwise HTML tags leak into the results.
Which ProductsAll ProductsThe whole catalog.
Inventory PolicyIgnore (demo) / Follow (production)Demo stores usually show everything regardless of stock.
Content LanguageYour products' languageMatch the storefront language and the widget's defaultLanguage.
Target Country / Currencye.g. United States / USD

1.3 Generate and check the feed

  1. Build the feed, then Regenerate for an instant build (otherwise it builds on the app's next scheduled cycle).
  2. Wait until the item count is greater than 0 and a feed link appears (a .xml URL).
  3. Open the URL and confirm: <g:id> is a plain number (the variant id), each variant is its own <item> sharing one <g:item_group_id>, and <g:price> looks like 120.00 USD.

Item count stuck at 0?

Usually it's the GTIN identifier option (it drops products without a barcode) or Inventory Policy set to Follow with zero stock.

The feed app dashboard (Pify shown). Copy the Feed Link — the .xml URL — once the Item Count is greater than 0. Use Regenerate for an instant rebuild instead of waiting for the daily cycle.

1.4 Connect the feed to SearchX

In the SearchX dashboard, on your application, set the XML feed type to Google and the feed URL to the .xml link from your feed app, then run the import. SearchX downloads the feed, parses it, and builds your search index.

In the SearchX application: set Feed Source to Google, paste the feed's .xml URL into XML Feed URL and Save & Import, and set the Origin URL to your Shopify domain (e.g. https://your-store.myshopify.com) so the widget is allowed to load there.

1.5 Filterable attributes

In your SearchX application settings, make sure sizes, color, brand, and price are listed as filterable attributes, or the Brands / Sizes / Price-range facets won't filter.


Step 2: Add the search bar to the header

The widget loads once, site-wide, from a Custom Liquid section inside the Header group (the header renders on every page, so the widget is available everywhere).

  1. Online Store → Themes → Customize.
  2. In the Header group, choose Add section → Custom Liquid.
  3. Paste the snippet below into the Liquid code field and Save. Replace YOUR_APP_ID, YOUR_PUBLIC_API_KEY, and customCSSUrl with your own values.
<style>
  /* Desktop: breathing room under the bar */
  @media (min-width: 750px) {
    #searchx-search-bar {
      margin-bottom: 28px;
      margin-top: 28px;
    }
  }
</style>

<!-- Hide the theme's native search icon so SearchX is the only search entry -->
<script>
  (function () {
    function hide() {
      document.querySelectorAll('.header-actions__action').forEach(function (b) {
        var a = (b.getAttribute('aria-label') || '').toLowerCase();
        if (a.indexOf('search') > -1) b.style.display = 'none';
      });
    }
    hide();
    document.addEventListener('DOMContentLoaded', hide);
    window.addEventListener('load', hide);
  })();
</script>

<!-- Where the search bar mounts -->
<div id="searchx-search-bar"></div>

<!-- Your credentials and options -->
<script>
  window.__SearchXDemo = {
    app_id: 'YOUR_APP_ID',
    api_key: 'YOUR_PUBLIC_API_KEY',
    apiPageURL: 'https://admin.searchxengine.ai/api/v1',
    customCSSUrl: 'https://your-cdn.example.com/searchx-theme.css',
    defaultLanguage: 'en',
    searchPageURL: '/search',
    cartAddUrl: '/cart/add.js',
  };
</script>

<!-- React + ReactDOM + SearchX SDK (CDN, deferred) -->
<script src="https://unpkg.com/react@18/umd/react.production.min.js" defer></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js" defer></script>
<script src="https://sdk.searchxengine.ai/searchx-sdk.umd.js" defer></script>

<!-- Boot the SDK + bridge Add to Cart to Shopify -->
<script defer>
  (function () {
    var cfg = window.__SearchXDemo;
    function ready() {
      return (
        window.SearchXSDK &&
        typeof window.SearchXSDK.init === 'function' &&
        window.React &&
        window.ReactDOM
      );
    }
    function present(id) {
      return document.getElementById(id);
    }
    var booted = false;
    function boot() {
      if (booted) return;
      var components = {};
      if (present('searchx-search-bar')) components.SearchBar = { root: 'searchx-search-bar' };
      if (present('searchx-search-page')) components.SearchPage = { root: 'searchx-search-page' };
      if (!Object.keys(components).length) return;
      window.SearchXSDK.init({
        app_id: cfg.app_id,
        api_key: cfg.api_key,
        apiPageURL: cfg.apiPageURL,
        defaultLanguage: cfg.defaultLanguage,
        components: components,
        settings: {
          platform: 'custom',
          searchPageURL: cfg.searchPageURL,
          aiSearchEnabled: true,
          showAddToCartButton: true,
          showPopupAddToCart: true,
          showBrand: true,
          showColorFilter: true,
          showSizeFilter: true,
          showPriceRangeFilter: true,
          mobileViewPort: '750px',
        },
        customCSSUrl: cfg.customCSSUrl,
      });
      booted = true;
    }
    var tries = 0;
    function wait() {
      if (ready()) {
        boot();
        return;
      }
      if (tries++ > 150) return;
      setTimeout(wait, 100);
    }

    // Add to Cart bridge — see Step 4
    window.addEventListener('SearchX:addToCart', function (e) {
      var product = (e && e.detail && e.detail.product) || {};
      var id = product.unique_id || product.variant_id || product.id;
      if (!id) return;
      var drawer =
        document.querySelector('cart-drawer') || document.querySelector('cart-notification');
      var body = { items: [{ id: id, quantity: 1 }] };
      if (drawer && typeof drawer.getSectionsToRender === 'function') {
        try {
          var ids = drawer
            .getSectionsToRender()
            .map(function (s) {
              return s.section || s.id;
            })
            .filter(Boolean);
          if (ids.length) {
            body.sections = ids;
            body.sections_url = window.location.pathname;
          }
        } catch (_) {}
      }
      fetch(cfg.cartAddUrl, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
        body: JSON.stringify(body),
      })
        .then(function (r) {
          return r.json().then(function (data) {
            return { ok: r.ok, data: data };
          });
        })
        .then(function (res) {
          if (!res.ok) {
            // Add failed (invalid variant, out of stock, ...): leave the cart UI untouched
            window.dispatchEvent(new CustomEvent('SearchX:addToCartError', { detail: res.data }));
            return;
          }
          var data = res.data;
          if (drawer && typeof drawer.renderContents === 'function') {
            drawer.renderContents(data);
            if (typeof drawer.open === 'function') {
              try {
                drawer.open();
              } catch (_) {}
            }
            return;
          }
          document.dispatchEvent(new CustomEvent('cart:refresh', { bubbles: true }));
          window.dispatchEvent(new CustomEvent('SearchX:cartUpdated', { detail: data }));
          var toggle = document.querySelector(
            '.js-drawer-open-cart,[data-cart-toggle],a[href="#cart"]',
          );
          if (toggle) {
            toggle.click();
            return;
          }
          window.location.reload();
        })
        .catch(function () {
          window.location.reload();
        });
    });

    if (document.readyState === 'loading') {
      document.addEventListener('DOMContentLoaded', wait);
    } else {
      wait();
    }
  })();
</script>
In Customize, the Custom Liquid section sits inside the Header group (left). The SearchX search bar renders in the header, and the snippet lives in the Liquid code panel (right).

What the key options do:

  • platform: 'custom' — the widget does not write to the cart itself; it fires a SearchX:addToCart event that the snippet handles (Step 4).
  • searchPageURL: '/search' — pressing Enter sends the shopper to the results page (Step 3).
  • mobileViewPort: '750px' — below this width the widget collapses to a compact magnifier (Step 5). The key is mobileViewPort (camelCase); any other spelling is ignored and falls back to the default.
  • customCSSUrl — an external stylesheet that themes the widget itself (colours, cards, buttons), separate from the layout CSS in the <style> block.

What to replace (and where to find it)

In the snippetReplace withWhere to find it
app_id: 'YOUR_APP_ID'Your Application IDSearchX dashboard → your application → Application ID
api_key: 'YOUR_PUBLIC_API_KEY'Your public search keyYour application → API Keys → copy the active key
defaultLanguage: 'en'Your store's language (optional)e.g. el for Greek
customCSSUrl: '...'URL of your widget stylesheet (optional)Your own CDN, or delete the line to use defaults — see Widget Styling
searchPageURL: '/search'Your results page pathOnly if your results page isn't at /search
The section id in the mobile @media blockYour section's real idThe ?section=... value in the Customize URL, or run document.getElementById('searchx-search-bar').closest('.shopify-section').id in the browser console

Leave these unchanged: apiPageURL, the three CDN <script> URLs, cartAddUrl (/cart/add.js is Shopify-standard), and the boot + Add to Cart logic.


Step 3: Results page at /search

You want /search to show only SearchX, not the theme's native search.

  1. In Customize, switch the template selector (top centre) to Search.

  2. In the Template group, Add section → Custom Liquid and set its Liquid code to just:

    <div id="searchx-search-page"></div>
    

    The SDK is already loaded by the header, so it mounts the results page here automatically.

  3. Save.

Then hide the theme's native search on this template: in the Template group there are usually two native sections — Search (title + input) and Search results (the grid). Select each, open the menu, and choose Hide. After that, /search shows only the SearchX results page with its facets, sorting, and pagination.

The Search template with a Custom Liquid section holding the results container. The native Search and Search results sections are hidden (struck-through eye icons), so only the SearchX results page renders — with Brands, Price range, Sizes, sorting, and pagination.

Step 4: Add to Cart

When a shopper clicks Add to Cart on a result, the widget fires:

window.dispatchEvent(new CustomEvent('SearchX:addToCart', { detail: { product: item } }));

The snippet listens for that event and always adds the item with a POST to Shopify's /cart/add.js. It then tries to refresh the cart UI, in this order:

  1. Live drawer update (best-effort). If the theme exposes a compatible cart drawer (<cart-drawer> or <cart-notification> with Shopify's Section Rendering hooks, as in some Dawn-family themes), the snippet requests the re-rendered drawer HTML and tries to update and open the drawer without a reload.
  2. Otherwise it broadcasts common cart events and tries to open a cart toggle.
  3. If neither applies, it reloads the page so the cart updates.

Live drawer update is theme-dependent

The in-place drawer update is best-effort and not guaranteed. On many themes — including the Horizon test store — the snippet falls back to a full page reload, which always reflects the new cart but isn't as smooth. If you want a specific theme's drawer to update in place, wire that theme's own cart/drawer API into the SearchX:addToCart handler in the snippet.

Add to Cart needs the variant id

/cart/add.js expects a Shopify variant id. That's why Step 1.2 sets Product ID Format = Variant ID — so each product's unique_id in SearchX equals the Shopify variant id and Add to Cart works per size. If you see "variant not found", this setting is the cause.

Shopify has no native wishlist, so the heart action is forwarded as a SearchX:wishlistRequested event you can hook into a wishlist app.


Step 5: Layout and mobile

All layout CSS lives in the <style> block at the top of the header Custom Liquid. Because the header renders everywhere, this CSS applies site-wide.

Desktop spacing

@media (min-width: 750px) {
  #searchx-search-bar {
    margin-bottom: 28px;
    margin-top: 28px;
  }
}

Mobile: stick the magnifier to the header

Below mobileViewPort (750px), the widget shows a compact magnifier instead of a full bar. You want it in the header row next to the burger, and staying with the header as the shopper scrolls.

The catch: in Horizon (and most Online Store 2.0 themes) only the <header> element is position: sticky — your Custom Liquid section is a sibling below it, not a child. So position: absolute or fixed won't stick: absolute is anchored to the page and scrolls away, and fixed pulls the section out of flow so its full-width background covers your content.

The fix is to make your section sticky too, pull it up into the header row, and cancel that pull so the rest of the page doesn't shift:

@media (max-width: 749px) {
  #shopify-section-sections--XXXX__custom_liquid_YYYY {
    position: sticky;
    top: -1px; /* match Horizon's sticky header offset */
    z-index: 9; /* above the header (its z-index is 8) */
    height: 0; /* no extra row */
    margin-top: -52px; /* pull the icon up into the header row */
    margin-bottom: 52px; /* cancel the pull so page content doesn't shift */
    overflow: visible;
  }
  #shopify-section-sections--XXXX__custom_liquid_YYYY .searchx__container {
    width: auto !important;
    justify-content: flex-start;
    margin-left: 21px; /* sit to the right of the burger */
    margin-top: 8px; /* fine vertical nudge, before the header is stuck */
  }
  /* When the header pins, Horizon shifts it a few px — nudge the icon to match */
  #shopify-section-sections--XXXX__custom_liquid_YYYY.sx-stuck .searchx__container {
    margin-top: 14px;
  }
  #searchx-search-bar {
    width: auto !important;
  }
}

Track the stuck state

CSS can't tell whether a sticky element is currently pinned, and Horizon's header shifts by a few pixels once it sticks — so the icon needs a slightly different margin-top at rest vs stuck. A tiny script adds an sx-stuck class to your own section while it's pinned (it only toggles a class on your element — it never touches the theme's header DOM). Add it once inside the header Custom Liquid:

<script>
  (function () {
    var sec = document.getElementById('shopify-section-sections--XXXX__custom_liquid_YYYY');
    if (!sec) return;
    function update() {
      sec.classList.toggle('sx-stuck', sec.getBoundingClientRect().top <= 0);
    }
    window.addEventListener('scroll', update, { passive: true });
    window.addEventListener('resize', update);
    update();
  })();
</script>

How to tune it:

  • Vertical (coarse): change the section's margin-top and margin-bottom together to the same magnitude — a larger negative margin-top (with an equal positive margin-bottom) lifts the icon higher without shifting page content.
  • Vertical (fine): the container's margin-top — set the rest value, and the .sx-stuck value for when the header is pinned.
  • Horizontal: the container's margin-left.
  • Still hidden behind the header? raise z-index (the header is 8).

The section id is unique to your store — find it from the Customize URL (?section=...) when the section is selected, or in the storefront with:

document.getElementById('searchx-search-bar').closest('.shopify-section').id;

Don't move the bar with JavaScript

Keep the CSS breakpoint and mobileViewPort in sync — if the CSS switches at 750px but mobileViewPort is still 900px, the layout and the widget disagree between those widths.

Also, never relocate #searchx-search-bar with JavaScript into the theme's header DOM. Themes like Horizon manage their header's children and will delete anything you inject, breaking the bar. Leave the bar in your own section and position it with CSS, as above.


Settings reference

The most common settings you'll tune in the snippet:

SettingPurpose
platformUse custom on Shopify so the cart is handled by the snippet's bridge.
searchPageURLWhere the search bar sends results on submit (/search).
mobileViewPortWidth at which the widget switches to the compact mobile layout (e.g. 750px).
aiSearchEnabledTurn the AI-assisted search experience on or off.
showAddToCartButtonShow the Add to Cart button on each product card.
showPopupAddToCartQuick Add to Cart inside the search-bar popup.
showBrandShow the brand on product cards.
showBrandFacet / showColorFilter / showSizeFilter / showPriceRangeFilterWhich facet panels appear in the results sidebar.

For the full list of options, see the SDK Reference and Search Settings.


Troubleshooting

SymptomCause / fix
Bar loads but shows 0 resultsInvalid or expired API key. Test with POST {apiPageURL}/applications/authenticate and Authorization: Bearer <key> — it must return 200.
Bar doesn't appear at allCheck React + the SDK + window.__SearchXDemo all loaded. In the console: document.getElementById('searchx-search-bar').innerHTML.length should be > 0.
/search shows duplicate resultsThe native Search / Search results sections aren't hidden (Step 3).
Add to Cart fails ("variant not found")unique_id isn't a variant id — set Product ID Format = Variant ID in the feed (Step 1.2).
Facets don't filterAdd sizes, color, brand, price to the application's filterable attributes (Step 1.5).
Mobile icon disappears after loadDon't move it with JS; position with CSS (Step 5). If invisible, raise z-index.
Feed URL returns 404The feed hasn't generated yet — click Regenerate in the feed app.

"Invalid HTML" warning on the Custom Liquid section

Shopify's theme editor lints the Custom Liquid field and may flag the inline <script> as invalid HTML. It is non-blocking — the storefront still works. To remove it, move the JavaScript into a theme asset file (Edit code → Assets → add searchx-init.js) and load it from the section with <script src="{{ 'searchx-init.js' | asset_url }}" defer></script>, leaving only markup and a small config object in the Custom Liquid block.


Checklist

  1. Feed app installed, configured per Step 1.2, Regenerated, .xml URL obtained.
  2. Feed verified: g:id is the variant id.
  3. SearchX application: feed type Google, feed URL set, import run.
  4. Filterable attributes: sizes, color, brand, price.
  5. Header group → Custom Liquid → header snippet pasted with your credentials.
  6. Search template → Custom Liquid with #searchx-search-page.
  7. Native Search + Search results sections hidden.
  8. Test: search → results → Add to Cart updates the cart.
  9. Desktop spacing and mobile magnifier positioned.
Previous
WooCommerce