@blotoutio/providers-evo-search-sdk 1.70.1 → 1.70.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +128 -0
- package/core.cjs.js +444 -14
- package/core.js +444 -14
- package/core.mjs +444 -14
- package/hooks.cjs.js +2901 -12
- package/hooks.d.ts +583 -1
- package/hooks.mjs +2898 -15
- package/index.cjs.js +8581 -215
- package/index.js +8581 -215
- package/index.mjs +8581 -215
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,6 +26,24 @@ export default function Root({ children }) {
|
|
|
26
26
|
}
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
+
### Multi-market storefronts: pass the market
|
|
30
|
+
|
|
31
|
+
On a Shopify theme the SDK reads the shopper's currency and country from
|
|
32
|
+
`window.Shopify`. That global does not exist on a headless storefront, so
|
|
33
|
+
without `market` every price renders in the merchant's single configured
|
|
34
|
+
currency (and quick-add validation returns prices for the merchant's default
|
|
35
|
+
market). Pass the market your own router resolved, and pass it again when the
|
|
36
|
+
shopper switches:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
<EvoSearchProvider market={{ currency: 'CAD', country: 'CA' }}>{children}</EvoSearchProvider>
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
It belongs on the provider rather than on a hook: one API object serves every
|
|
43
|
+
EvoSearch surface on the page, so the market is a property of the page — every
|
|
44
|
+
hook picks it up. To set it from a component instead, call
|
|
45
|
+
`useEvoSearchAPI().api?.setMarket({ currency, country })`.
|
|
46
|
+
|
|
29
47
|
Use the hook in any search component:
|
|
30
48
|
|
|
31
49
|
```tsx
|
|
@@ -54,6 +72,106 @@ function HeaderSearch() {
|
|
|
54
72
|
}
|
|
55
73
|
```
|
|
56
74
|
|
|
75
|
+
### Full results route
|
|
76
|
+
|
|
77
|
+
Two options, and the difference matters on a headless storefront:
|
|
78
|
+
|
|
79
|
+
| | `EvoSearchSearchPage` | `useEvoSearchResults` |
|
|
80
|
+
| --------------------------------- | ------------------------------------- | ------------------------------- |
|
|
81
|
+
| Markup | the merchant's published design | yours |
|
|
82
|
+
| Merchant's **Search page** toggle | required — nothing renders without it | ignored; retrieval always works |
|
|
83
|
+
|
|
84
|
+
**`EvoSearchSearchPage`** renders the managed client-rendered page. It can only
|
|
85
|
+
draw the merchant's published design, so it renders `fallback` whenever that
|
|
86
|
+
design is unavailable — the SDK has not registered, the merchant's results-page
|
|
87
|
+
toggle is off, nothing is published yet, the first search is still in flight, or
|
|
88
|
+
that first search failed. **`fallback` defaults to nothing, so a route with no
|
|
89
|
+
fallback renders blank in every one of those cases.** Always pass your native
|
|
90
|
+
search UI:
|
|
91
|
+
|
|
92
|
+
```tsx
|
|
93
|
+
import { EvoSearchSearchPage } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
94
|
+
|
|
95
|
+
function SearchRoute({ location }: { location: Location }) {
|
|
96
|
+
return (
|
|
97
|
+
<EvoSearchSearchPage
|
|
98
|
+
initialSearch={location.search}
|
|
99
|
+
onUrlChange={(path) => window.history.replaceState(null, '', path)}
|
|
100
|
+
fallback={<NativeSearchResults />}
|
|
101
|
+
// Result tiles otherwise link to the Shopify Online Store URL the catalog
|
|
102
|
+
// was ingested with — a different domain from a headless storefront.
|
|
103
|
+
productHref={(product, variantId) => `/products/${product.handle}${variantId ? `?variant=${variantId}` : ''}`}
|
|
104
|
+
onAddToCart={(product, variantId) => addLine(variantId)}
|
|
105
|
+
/>
|
|
106
|
+
)
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
In development it logs which of those reasons applied, once per reason. The v1
|
|
111
|
+
managed page is client-rendered. Pass updated URL-derived state when your router
|
|
112
|
+
handles back/forward navigation.
|
|
113
|
+
|
|
114
|
+
**`useEvoSearchResults`** is the one to use for a search page you render
|
|
115
|
+
yourself. It provides URL-ready query, filter, sort, pagination, collection,
|
|
116
|
+
loading, and error state while leaving routing and markup under the storefront's
|
|
117
|
+
control. It does **not** wait on the merchant's results-page toggle — that
|
|
118
|
+
setting governs the managed renderer's design, and gating your own markup on it
|
|
119
|
+
would leave your search page silently empty. `searchPageEnabled` is still
|
|
120
|
+
returned if you want to mirror it.
|
|
121
|
+
|
|
122
|
+
```tsx
|
|
123
|
+
import { parseSearchUrl, useEvoSearchResults } from '@blotoutio/providers-evo-search-sdk/hooks'
|
|
124
|
+
|
|
125
|
+
function SearchPage({ initialSearch }: { initialSearch: string }) {
|
|
126
|
+
const initial = parseSearchUrl(initialSearch)
|
|
127
|
+
const search = useEvoSearchResults({
|
|
128
|
+
initialQuery: initial.query,
|
|
129
|
+
initialFilters: initial.filters,
|
|
130
|
+
initialSort: initial.sort,
|
|
131
|
+
onUrlChange: (path) => window.history.replaceState(null, '', path),
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
if (!search.ready) return <p>Preparing search…</p>
|
|
135
|
+
if (search.error) return <p>Search is unavailable. Try again.</p>
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<main>
|
|
139
|
+
<input aria-label='Search products' value={search.query} onChange={(event) => search.setQuery(event.target.value)} />
|
|
140
|
+
{search.loading && <span>Searching…</span>}
|
|
141
|
+
|
|
142
|
+
<select aria-label='Sort products' value={search.sort} onChange={(event) => search.setSort(event.target.value as typeof search.sort)}>
|
|
143
|
+
<option value='relevance'>Relevance</option>
|
|
144
|
+
<option value='price-asc'>Price: low to high</option>
|
|
145
|
+
<option value='price-desc'>Price: high to low</option>
|
|
146
|
+
<option value='newest'>Newest</option>
|
|
147
|
+
</select>
|
|
148
|
+
|
|
149
|
+
<p aria-live='polite'>{search.totalCount} products</p>
|
|
150
|
+
<ul>
|
|
151
|
+
{search.results.map((product) => (
|
|
152
|
+
<li key={product.id}>
|
|
153
|
+
<a href={product.url} onClick={() => search.trackClick(product.id)}>
|
|
154
|
+
{product.title}
|
|
155
|
+
</a>
|
|
156
|
+
</li>
|
|
157
|
+
))}
|
|
158
|
+
</ul>
|
|
159
|
+
|
|
160
|
+
{search.hasMore && (
|
|
161
|
+
<button type='button' disabled={search.loadingMore} onClick={search.loadMore}>
|
|
162
|
+
{search.loadingMore ? 'Loading…' : 'Load more'}
|
|
163
|
+
</button>
|
|
164
|
+
)}
|
|
165
|
+
</main>
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
For server-rendered routers, derive the initial state from the request URL and
|
|
171
|
+
pass your router's navigation function as `onUrlChange`. The hook does not
|
|
172
|
+
write to browser history on its own. When the query is cleared, its associated
|
|
173
|
+
filters are cleared too; the shopper's chosen sort is kept.
|
|
174
|
+
|
|
57
175
|
## Image search
|
|
58
176
|
|
|
59
177
|
Shoppers can search with a photo instead of words. The hook exposes it as one extra action that writes the same `results` / `loading` / `error` state as the text path, so you render one result list:
|
|
@@ -212,6 +330,16 @@ When you want to render search results with your own React components instead of
|
|
|
212
330
|
|
|
213
331
|
The EdgeTag pixel must still be loaded on your storefront for the SDK to register and the React hook to receive an API.
|
|
214
332
|
|
|
333
|
+
**Leaving this toggle off on a headless storefront is not neutral.** `index.min.js` loads the Lit element, which on a `/search` route tries to inject its own results view into `#ProductGridContainer`, `#product-grid`, `results-list` or `.product-grid-container` — if your app happens to use one of those, it fights React for the same DOM. It also navigates with a full page load instead of your router, reports every request on the `widget` channel so your headless traffic is mislabelled in analytics, and skips the keep-warm ping that only runs in headless mode.
|
|
334
|
+
|
|
335
|
+
## Headless prerequisites checklist
|
|
336
|
+
|
|
337
|
+
- **Disable default UI** — on (above).
|
|
338
|
+
- **Product source** — set to `allActive`. The default (`onlineStore`) only syncs products published to the Online Store, which is meaningless on a headless storefront: measured on a live headless tenant, it dropped 56% of the catalog.
|
|
339
|
+
- **Search page** — needed only for `EvoSearchSearchPage`; `useEvoSearchResults` works without it.
|
|
340
|
+
- **EdgeTag loader** — installed on the storefront, not just the Shopify app.
|
|
341
|
+
- **Package version** — the hooks ship from npm, but the API object they call is created by the bundle your tag's worker serves. If the deployed bundle predates a method the hooks need, they log which one; redeploy the tag, or pin the package to the deployed version.
|
|
342
|
+
|
|
215
343
|
`api.search()` retries transient failures and then rejects with a `TimeoutError` after ~23 seconds rather than hanging, so handle the rejected promise. Pass an `AbortSignal` when a newer query can supersede an older one — it is the only way to cancel pending retries, and the React hook already does this for you.
|
|
216
344
|
|
|
217
345
|
## /load endpoint cache
|