@algolia/wizard 0.45.0-rc.141.320 → 0.45.0
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/dist/main.js
CHANGED
|
@@ -3368,22 +3368,59 @@ var detectLanguageSchema = z21.object({
|
|
|
3368
3368
|
`The prefix a bundler/framework requires for an env var to reach client-side code (e.g. "NEXT_PUBLIC_", "VITE_", "NUXT_PUBLIC_"), uppercase with a trailing underscore when non-empty. Prefer the project's own existing convention over a framework default. Empty string when the project has no client-side env var exposure (e.g. a backend-only project).`
|
|
3369
3369
|
)
|
|
3370
3370
|
});
|
|
3371
|
-
var
|
|
3372
|
-
|
|
3373
|
-
|
|
3374
|
-
|
|
3375
|
-
|
|
3376
|
-
|
|
3377
|
-
|
|
3378
|
-
|
|
3379
|
-
|
|
3380
|
-
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3371
|
+
var FRONTEND_FRAMEWORKS = /* @__PURE__ */ new Set([
|
|
3372
|
+
"angular",
|
|
3373
|
+
"astro",
|
|
3374
|
+
"ember",
|
|
3375
|
+
"javascript",
|
|
3376
|
+
"next",
|
|
3377
|
+
"nextjs",
|
|
3378
|
+
"nuxt",
|
|
3379
|
+
"preact",
|
|
3380
|
+
"qwik",
|
|
3381
|
+
"react",
|
|
3382
|
+
"remix",
|
|
3383
|
+
"solid",
|
|
3384
|
+
"solidjs",
|
|
3385
|
+
"svelte",
|
|
3386
|
+
"sveltekit",
|
|
3387
|
+
"vanilla",
|
|
3388
|
+
"vanillajs",
|
|
3389
|
+
"vue",
|
|
3390
|
+
"vuejs"
|
|
3391
|
+
]);
|
|
3392
|
+
var normalizeFramework = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
3393
|
+
function prioritizeFrontendFrameworks(result) {
|
|
3394
|
+
const frontend = result.frameworks.filter(
|
|
3395
|
+
(framework) => FRONTEND_FRAMEWORKS.has(normalizeFramework(framework.name))
|
|
3396
|
+
);
|
|
3397
|
+
if (frontend.length === 0) return result;
|
|
3398
|
+
const remaining = result.frameworks.filter(
|
|
3399
|
+
(framework) => !FRONTEND_FRAMEWORKS.has(normalizeFramework(framework.name))
|
|
3400
|
+
);
|
|
3401
|
+
return { ...result, frameworks: [...frontend, ...remaining] };
|
|
3402
|
+
}
|
|
3403
|
+
var detectLanguage = async () => {
|
|
3404
|
+
const result = await runAgent({
|
|
3405
|
+
instructions: [
|
|
3406
|
+
"Analyze the codebase and determine the programming languages and frameworks used",
|
|
3407
|
+
"If a superset language is found, exclude the subset language (e.g. TypeScript over JavaScript).",
|
|
3408
|
+
"For frameworks, inspect the code that implements the UI and prioritize the frontend framework used by the primary UI.",
|
|
3409
|
+
"When a separate frontend framework exists, omit backend frameworks (e.g. report React, not Rails, when Rails serves a React UI). If no separate frontend framework exists, report the server framework that renders the UI.",
|
|
3410
|
+
"If a frontend meta-framework is used, exclude the framework it builds on (e.g. Next.js over React).",
|
|
3411
|
+
"Return the exact version",
|
|
3412
|
+
"Exclude things like CSS frameworks, build tools, or testing frameworks",
|
|
3413
|
+
`Determine publicEnvVarPrefix: check the project's own env var usage first (e.g. names already referenced in code, .env/.env.example); if none exists, fall back to the detected framework's known convention for exposing env vars to client-side code; use "" when the project has no such convention (e.g. a backend-only project).`,
|
|
3414
|
+
"A brand-new project has no existing env var usage to find \u2014 one or two targeted checks (e.g. .env/.env.example, or a grep for the bundler's public-prefix convention) are enough to confirm that. Do not keep searching once those turn up nothing; fall back to the framework convention immediately.",
|
|
3415
|
+
'Use as few tools as possible, but do not guess. If you cant find the answer, say "unknown"',
|
|
3416
|
+
"When done, call reportStatus"
|
|
3417
|
+
],
|
|
3418
|
+
tools: ["listFiles", "changeDirectory", "readFile", "searchFiles"],
|
|
3419
|
+
outputSchema: detectLanguageSchema,
|
|
3420
|
+
modelSize: "small"
|
|
3421
|
+
});
|
|
3422
|
+
return prioritizeFrontendFrameworks(result);
|
|
3423
|
+
};
|
|
3387
3424
|
|
|
3388
3425
|
// src/actions/analyzeCodebase.ts
|
|
3389
3426
|
import z22 from "zod";
|
|
@@ -3472,7 +3509,7 @@ async function runAnalysis(mode, extraInstructions = []) {
|
|
|
3472
3509
|
// package.json
|
|
3473
3510
|
var package_default = {
|
|
3474
3511
|
name: "@algolia/wizard",
|
|
3475
|
-
version: "0.45.0
|
|
3512
|
+
version: "0.45.0",
|
|
3476
3513
|
description: "Magically implement Algolia functionality in your codebase",
|
|
3477
3514
|
type: "module",
|
|
3478
3515
|
engines: {
|
|
@@ -3661,7 +3698,7 @@ function confirmed(name, version) {
|
|
|
3661
3698
|
return { frameworks };
|
|
3662
3699
|
}
|
|
3663
3700
|
async function askOtherFramework(ctx) {
|
|
3664
|
-
let prompt = "enter your
|
|
3701
|
+
let prompt = "enter the framework used for your frontend";
|
|
3665
3702
|
for (; ; ) {
|
|
3666
3703
|
const answer = await ctx.requestUserInput({
|
|
3667
3704
|
prompt,
|
|
@@ -3673,7 +3710,7 @@ async function askOtherFramework(ctx) {
|
|
|
3673
3710
|
}
|
|
3674
3711
|
const name = parseEntries(answer)[0]?.name;
|
|
3675
3712
|
if (name) return name;
|
|
3676
|
-
prompt = "please enter
|
|
3713
|
+
prompt = "please enter the framework used for your frontend:";
|
|
3677
3714
|
}
|
|
3678
3715
|
}
|
|
3679
3716
|
async function confirmFramework(ctx) {
|
|
@@ -3688,9 +3725,12 @@ async function confirmFramework(ctx) {
|
|
|
3688
3725
|
const primary = detectedFrameworks[0];
|
|
3689
3726
|
if (primary) {
|
|
3690
3727
|
const accepted = await ctx.requestUserInput({
|
|
3691
|
-
prompt: `Use ${primary.name}?`,
|
|
3728
|
+
prompt: `Use ${primary.name} for your frontend?`,
|
|
3692
3729
|
promptType: "acceptReject",
|
|
3693
|
-
options: [
|
|
3730
|
+
options: [
|
|
3731
|
+
`Confirm ${primary.name}`,
|
|
3732
|
+
"Use a different framework for your frontend"
|
|
3733
|
+
],
|
|
3694
3734
|
secondary: [{ kind: "badge", value: "[DETECTED]" }, void 0]
|
|
3695
3735
|
});
|
|
3696
3736
|
if (accepted === true) return confirmed(primary.name, primary.version);
|
|
@@ -3703,7 +3743,7 @@ async function confirmFramework(ctx) {
|
|
|
3703
3743
|
0
|
|
3704
3744
|
);
|
|
3705
3745
|
const selection = await ctx.requestUserInput({
|
|
3706
|
-
prompt: "select a framework",
|
|
3746
|
+
prompt: "select a framework for your frontend",
|
|
3707
3747
|
promptType: "multipleChoice",
|
|
3708
3748
|
options,
|
|
3709
3749
|
secondary,
|
|
@@ -4184,7 +4224,6 @@ function searchInstructions(input) {
|
|
|
4184
4224
|
return [
|
|
4185
4225
|
"Implement an in-app Algolia search experience.",
|
|
4186
4226
|
`Build the search UI for ${input.searchUiTarget}.`,
|
|
4187
|
-
"Search UI only \u2014 the index is already populated. Do not create or modify ingestion scripts, rake/manage/CLI tasks, migrations, seeders, or other data-loading code, even if the data looks incomplete.",
|
|
4188
4227
|
...doc ? [
|
|
4189
4228
|
"Follow the Algolia SDK reference below for client setup, search UI wiring, and Insights instrumentation \u2014 Insights is required, not optional; prefer the reference over prior knowledge:",
|
|
4190
4229
|
doc
|
|
@@ -4192,15 +4231,9 @@ function searchInstructions(input) {
|
|
|
4192
4231
|
"No bundled Algolia SDK reference exists for this stack, so rely on the project's own conventions and Algolia's official client for its language. Do not invent APIs \u2014 keep to the documented search endpoint and its parameters."
|
|
4193
4232
|
],
|
|
4194
4233
|
`Create the search experience as its own component in a new file, following the project's existing component conventions (location, naming, styling approach). Do not write it inline into an existing file.`,
|
|
4195
|
-
`Import and render that new component from ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results
|
|
4234
|
+
`Import and render that new component from ${input.searchLocation ? `"${input.searchLocation}"` : "the best shared, always-rendered layout location (e.g. a header/nav component)"} so it is reachable across the app \u2014 at least a working search input and results list against the target index.`,
|
|
4196
4235
|
"If a search box already exists, replace its usage with an import and render of your new component; remove the old implementation.",
|
|
4197
4236
|
"When rendering results with an existing shared component (e.g. a card), import and reuse that component rather than inlining its markup \u2014 inlining silently drops the styles and behavior its own file provides.",
|
|
4198
|
-
"Results go in a dropdown panel, not a permanently visible list. If the SDK reference above documents an Autocomplete widget for this stack, it already owns panel open/close, Escape, blur, outside click, arrow-key navigation, combobox ARIA, and Insights events \u2014 use it as-is and add no open/close logic, query gating, or ARIA roles of your own, even if it opens on focus before anything is typed. Build it by hand only where the reference has no such widget or calls it headless: then open the panel only while the input is focused with a non-empty query, close it on Escape, blur, and outside click, and move an active-result highlight with Up/Down, Enter to select.",
|
|
4199
|
-
"Let the widget own selection: where hits have a detail page, pass its per-index URL option (getURL or the flavor's equivalent) so it navigates on click and on Enter with a result highlighted, and keep your item template or component to content only \u2014 no anchor, no href, no click handler of your own, no stopPropagation. The widget's result element sends the Insights click event, closes the panel, and clears the active option; your own link swallows all three while still navigating, so it looks correct and silently loses click analytics.",
|
|
4200
|
-
"Render the zero-results state through the widget's own no-results template or component, so a query matching nothing shows a message rather than an empty panel.",
|
|
4201
|
-
"Meet WCAG AA contrast (4.5:1 body text, 3:1 large text/icons) between the panel's text and its own background, and give the input and the active result a focus indicator visible against whatever sits behind it. Style the panel through the widget's class and CSS-variable overrides (or, when hand-built, the app's existing theme tokens) \u2014 never assume a light surface or reuse the surrounding page's colors unchanged inside the panel.",
|
|
4202
|
-
"Vendor theme CSS, and any CSS you write against vendor class names, must be global: import the theme from the component's own script module or the app's global stylesheet, and put overrides in a global block (Astro <style is:global>, an unscoped Vue block, a plain global CSS file). Never a framework-scoped style block or a CSS Module \u2014 scoping rewrites the vendor selectors and the widget's runtime DOM carries no scope attribute, so not one rule matches: styling silently does nothing and the build still passes.",
|
|
4203
|
-
"The panel takes its width from the input by default, so a small input makes it unreadably narrow: give it a min-width of 320px independent of the input, anchored to the input edge it opens from so widening does not push it off-screen.",
|
|
4204
4237
|
`Define ${SEARCH_CONFIG_APP_ID}, ${SEARCH_CONFIG_SEARCH_KEY}, and ${SEARCH_CONFIG_INDEX_NAME} as exported constants in a module that fits this project's existing conventions for shared client-side config \u2014 reuse an existing one if it already holds config like this, or add a small new one otherwise. These are PUBLIC values, safe to commit and expose client-side: never read them from an environment variable or a .env* file, and never hardcode them anywhere except in that one module (import them wherever the search client needs them).`,
|
|
4205
4238
|
`Set ${SEARCH_CONFIG_APP_ID} to "${input.appId}" and ${SEARCH_CONFIG_INDEX_NAME} to "${input.targetIndex}".`,
|
|
4206
4239
|
input.searchKey ? `Set ${SEARCH_CONFIG_SEARCH_KEY} to "${input.searchKey}".` : `A real search-only key could not be provisioned${input.searchKeyError ? ` (${input.searchKeyError})` : ""} \u2014 set ${SEARCH_CONFIG_SEARCH_KEY} to the placeholder "${SEARCH_KEY_PLACEHOLDER}" and add a prominent TODO for the developer to fill in a real one.`,
|
|
@@ -20,20 +20,14 @@ Always install the **latest stable** within the major (use a caret range like `^
|
|
|
20
20
|
|
|
21
21
|
- Use a **search-only API key** in any browser/client code. It is safe to expose.
|
|
22
22
|
NEVER ship an admin or any write-capable key to the client.
|
|
23
|
-
-
|
|
24
|
-
|
|
25
|
-
them from an env var or `.env*` file, and never hardcode them elsewhere.
|
|
23
|
+
- Read the Application ID and search-only key from **public env vars**, never hardcode
|
|
24
|
+
them inline.
|
|
26
25
|
- Instantiate the search client **once, outside your components**, and pass a stable
|
|
27
26
|
reference. Do not inline `algoliasearch(...)` as a prop value — it breaks the
|
|
28
27
|
client cache and causes re-renders.
|
|
29
28
|
- For InstantSearch, import the client from `algoliasearch/lite` (smaller bundle and
|
|
30
29
|
correct types — see `instantsearch-setup.md`).
|
|
31
30
|
- Always enable **Insights** on the InstantSearch root with `insights: true`. Without it there are no click/conversion events, so many Algolia features will not work as expected.
|
|
32
|
-
- Vendor CSS must be **global**: import an InstantSearch theme from a JS/TS module or the
|
|
33
|
-
app's global stylesheet and keep `.ais-*` overrides in a global block. In a scoped style
|
|
34
|
-
block (Astro `<style>`, Vue `<style scoped>`, Svelte `<style>`) or a CSS Module, scoping
|
|
35
|
-
rewrites the selectors, the widget's runtime DOM carries no scope attribute, and the
|
|
36
|
-
styling silently does nothing while the build passes.
|
|
37
31
|
|
|
38
32
|
## Files
|
|
39
33
|
|
|
@@ -20,18 +20,10 @@ const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
|
20
20
|
|
|
21
21
|
## Vanilla (`instantsearch.js` v4)
|
|
22
22
|
|
|
23
|
-
Use the `autocomplete` widget for the results panel. It owns focus, blur, outside-click
|
|
24
|
-
dismissal, Up/Down navigation with wrap-around, Enter/Escape/Tab, and the combobox ARIA
|
|
25
|
-
(`combobox`, `aria-expanded`, `aria-activedescendant`) — add no open/close logic or ARIA
|
|
26
|
-
roles of your own.
|
|
27
|
-
|
|
28
|
-
Needs `instantsearch.js` **4.110.0+**. If an older 4.x is pinned, upgrade within `^4`
|
|
29
|
-
rather than switching widgets.
|
|
30
|
-
|
|
31
23
|
```js
|
|
32
24
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
33
25
|
import instantsearch from 'instantsearch.js'
|
|
34
|
-
import {
|
|
26
|
+
import { searchBox, hits } from 'instantsearch.js/es/widgets'
|
|
35
27
|
import {
|
|
36
28
|
ALGOLIA_APP_ID,
|
|
37
29
|
ALGOLIA_SEARCH_API_KEY,
|
|
@@ -47,140 +39,24 @@ const search = instantsearch({
|
|
|
47
39
|
})
|
|
48
40
|
|
|
49
41
|
search.addWidgets([
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
placeholder: 'Search...',
|
|
53
|
-
indices: [
|
|
54
|
-
{
|
|
55
|
-
indexName: ALGOLIA_INDEX_NAME,
|
|
56
|
-
getURL: (item) => `/products/${item.handle}`,
|
|
57
|
-
templates: {
|
|
58
|
-
item({ item, onSelect }, { html, components }) {
|
|
59
|
-
return html`<div onClick=${onSelect}>
|
|
60
|
-
${components.Highlight({ hit: item, attribute: 'name' })}
|
|
61
|
-
</div>`
|
|
62
|
-
},
|
|
63
|
-
noResults(_, { html }) {
|
|
64
|
-
return html`<span>No results</span>`
|
|
65
|
-
},
|
|
66
|
-
},
|
|
67
|
-
},
|
|
68
|
-
],
|
|
69
|
-
}),
|
|
42
|
+
searchBox({ container: '#searchbox' }),
|
|
43
|
+
hits({ container: '#hits' }),
|
|
70
44
|
])
|
|
71
45
|
|
|
72
46
|
search.start()
|
|
73
47
|
```
|
|
74
48
|
|
|
75
|
-
### Navigation is `getURL`'s job, not the template's
|
|
76
|
-
|
|
77
|
-
- `indices[].getURL` — `(item) => string`, a route that already exists; don't invent one.
|
|
78
|
-
It navigates on click **and** on Enter over a highlighted item, and records the click
|
|
79
|
-
event. Without it, Enter on a highlight does nothing.
|
|
80
|
-
- The `item` template renders content only: **no `<a>`, no `href`, no click handler of
|
|
81
|
-
your own, never `stopPropagation`.** The widget's `<li>` owns the click — it sends the
|
|
82
|
-
Insights `click`, closes the panel, clears the active option. An anchor wrapper swallows
|
|
83
|
-
all three while still navigating: looks correct, click analytics never fire, panel stays
|
|
84
|
-
open behind the new page.
|
|
85
|
-
- `templates.noResults` on every index, or a query matching nothing renders an empty box.
|
|
86
|
-
- `getSearchPageURL` sends Enter with nothing highlighted to a full results page; pass it
|
|
87
|
-
only if the app has one. `.getQuery` is optional. Leave `showQuerySuggestions`,
|
|
88
|
-
`showPromptSuggestions`, `showRecent`, and `aiMode` unset — each needs an index or
|
|
89
|
-
feature (query suggestions, recent-search storage, AI chat) this project lacks.
|
|
90
|
-
- TypeScript: type records through the generic, not casts inside templates —
|
|
91
|
-
`type ProductHit = Hit<{ title: string; handle: string }>` (`Hit` from
|
|
92
|
-
`instantsearch.js`), then `autocomplete<ProductHit>({ ... })` types `item`, `getURL`,
|
|
93
|
-
and the templates.
|
|
94
|
-
|
|
95
|
-
### Styling the panel
|
|
96
|
-
|
|
97
|
-
The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
|
|
98
|
-
component's own JS/TS module or the app's global stylesheet:
|
|
99
|
-
|
|
100
|
-
```js
|
|
101
|
-
import 'instantsearch.css/themes/satellite.css'
|
|
102
|
-
```
|
|
103
|
-
|
|
104
|
-
The theme and your `.ais-*` overrides must be **global**: never a CSS `@import` inside a
|
|
105
|
-
framework-scoped style block (Astro `<style>`, Vue `<style scoped>`, Svelte `<style>`) or
|
|
106
|
-
a CSS Module. Scoping rewrites every selector
|
|
107
|
-
(`.ais-AutocompleteInput[data-astro-cid-abc123]`) and the widget builds its DOM at runtime
|
|
108
|
-
with no scope attribute, so not one rule matches — unstyled UI, no error, build passes.
|
|
109
|
-
Use `<style is:global>` (Astro), an unscoped block or `:deep()` (Vue), or plain global CSS.
|
|
110
|
-
|
|
111
|
-
The theme ships a dark palette behind `:root[data-theme=dark], .dark`: in a dark app set
|
|
112
|
-
`data-theme="dark"` on `<html>` or render under a `.dark` ancestor, then adjust — don't
|
|
113
|
-
redefine the light palette by hand.
|
|
114
|
-
|
|
115
|
-
Palette lives in `--ais-*` custom properties on `:root` as **RGB triplets consumed through
|
|
116
|
-
`rgba()`**, not CSS colors — override with triplets or every widget color breaks:
|
|
117
|
-
|
|
118
|
-
```css
|
|
119
|
-
:root {
|
|
120
|
-
--ais-background-color-rgb: 24, 24, 27; /* not #18181b */
|
|
121
|
-
--ais-text-color-rgb: 244, 244, 245;
|
|
122
|
-
--ais-primary-color-rgb: 129, 140, 248;
|
|
123
|
-
}
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
Everything else is plain CSS against the widget's classes, or `cssClasses` /
|
|
127
|
-
`indices[].cssClasses` to add your own names to the same elements. Who owns what:
|
|
128
|
-
|
|
129
|
-
- `.ais-Autocomplete` — root. Its **container** sets the widget's width; don't size the
|
|
130
|
-
widget from the input.
|
|
131
|
-
- `.ais-AutocompleteForm` — visible frame: background, border, radius, focus ring
|
|
132
|
-
(`:focus-within`). The input's pill goes here.
|
|
133
|
-
- `.ais-AutocompleteInput` — text only; theme sets `border: 0`, `background: none`,
|
|
134
|
-
`width: 100%`. A border or width here leaves the form's frame drawn around it and
|
|
135
|
-
changes no widths.
|
|
136
|
-
- `.ais-AutocompletePanel` (+ `--open`), `.ais-AutocompletePanelLayout` — dropdown surface
|
|
137
|
-
and its scroll container.
|
|
138
|
-
- `.ais-AutocompleteIndex`, `-IndexHeader`, `-IndexList`, `-IndexItem`, `-IndexNoResults`
|
|
139
|
-
— one index's results; the active item carries `aria-selected="true"`.
|
|
140
|
-
- `.ais-AutocompleteItemWrapper`, `-ItemContent`, `-ItemIcon` — inside one result.
|
|
141
|
-
- `.ais-AutocompleteSubmitButton`, `-ClearButton`, `-LoadingIndicator`, `-Label` — form
|
|
142
|
-
controls.
|
|
143
|
-
- `.ais-AutocompleteDetached*` — detached mode (below).
|
|
144
|
-
|
|
145
|
-
Style nothing outside that list: `.ais-SearchBox-*` belongs to `searchBox` and does
|
|
146
|
-
nothing here. Verify names against the installed version:
|
|
147
|
-
`grep -o 'ais-Autocomplete[A-Za-z-]*' node_modules/instantsearch.css/themes/satellite.css | sort -u`.
|
|
148
|
-
|
|
149
|
-
The theme's panel is `position: absolute; width: 100%`, so it inherits the input
|
|
150
|
-
container's width — a small input yields an unreadably narrow panel. Give it a min-width
|
|
151
|
-
and anchor it to the edge it opens from, or it grows off-screen:
|
|
152
|
-
|
|
153
|
-
```css
|
|
154
|
-
.ais-AutocompletePanel {
|
|
155
|
-
min-width: 320px;
|
|
156
|
-
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
157
|
-
}
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
Below `(max-width: 680px)` the widget goes detached (fullscreen): the inline form becomes
|
|
161
|
-
a `.ais-AutocompleteDetachedSearchButton`, form and results move into an overlay
|
|
162
|
-
(`.ais-AutocompleteDetachedOverlay`, `.ais-AutocompleteDetachedContainer`,
|
|
163
|
-
`.ais-AutocompleteDetachedFormContainer`). So panel-width rules apply only above that
|
|
164
|
-
breakpoint, and styling the inline form leaves the mobile trigger button on the theme's
|
|
165
|
-
look unless you style it too. Change it with `detachedMediaQuery` or
|
|
166
|
-
`--ais-autocomplete-detached-media-query`; pass `''` to disable detached mode entirely.
|
|
167
|
-
|
|
168
49
|
## Insights (click analytics) — always enable it
|
|
169
50
|
|
|
170
51
|
The `insights` option turns on the Insights middleware (`instantsearch.js` v4.55+),
|
|
171
52
|
which adds `clickAnalytics: true` to every query and keeps the `userToken` in sync
|
|
172
53
|
between search and event calls. Never set `clickAnalytics` or `userToken` on the `configure` widget by hand.
|
|
173
54
|
|
|
174
|
-
Use `insights: true
|
|
175
|
-
|
|
176
|
-
`window.aa`.
|
|
55
|
+
Use `insights: true`. The middleware loads `search-insights` from the jsDelivr CDN
|
|
56
|
+
itself; do not install the package or pass `insightsClient`.
|
|
177
57
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
`insights: true` is the whole implementation.
|
|
181
|
-
|
|
182
|
-
Only if the project has a Content-Security-Policy blocking jsDelivr (the CDN script then
|
|
183
|
-
cannot load): install `search-insights` from npm and pass its default export as
|
|
58
|
+
If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
|
|
59
|
+
case, install `search-insights` from npm and pass its default export as
|
|
184
60
|
`insightsClient` in the `insights` option:
|
|
185
61
|
|
|
186
62
|
```js
|
|
@@ -214,16 +90,27 @@ insights: {
|
|
|
214
90
|
|
|
215
91
|
### Click and conversion events
|
|
216
92
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
(close the panel, record a recent search); the click event fires either way.
|
|
93
|
+
The `hits` and `infiniteHits` widgets send `view` and `click` events on their own. A
|
|
94
|
+
custom `item` template whose click target is your own element gets `sendEvent` as the
|
|
95
|
+
second argument:
|
|
221
96
|
|
|
222
|
-
|
|
223
|
-
|
|
97
|
+
```js
|
|
98
|
+
hits({
|
|
99
|
+
container: '#hits',
|
|
100
|
+
templates: {
|
|
101
|
+
item(hit, { html, sendEvent }) {
|
|
102
|
+
return html`<a
|
|
103
|
+
href="${hit.url}"
|
|
104
|
+
onClick="${() => sendEvent('click', hit, 'Hit Clicked')}"
|
|
105
|
+
>${hit.name}</a
|
|
106
|
+
>`
|
|
107
|
+
},
|
|
108
|
+
},
|
|
109
|
+
})
|
|
110
|
+
```
|
|
224
111
|
|
|
225
|
-
Conversions are business actions (add to cart, signup, booking)
|
|
226
|
-
|
|
112
|
+
Conversions are business actions (add to cart, signup, booking), so wire them at the
|
|
113
|
+
real action, not at render:
|
|
227
114
|
|
|
228
115
|
```js
|
|
229
116
|
sendEvent('conversion', hit, 'Product Added To Cart', {
|
|
@@ -20,17 +20,9 @@ const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
|
20
20
|
|
|
21
21
|
## React (`react-instantsearch` v7)
|
|
22
22
|
|
|
23
|
-
Use the `Autocomplete` widget for the results panel. It owns focus, blur, outside-click
|
|
24
|
-
dismissal, Up/Down navigation with wrap-around, Enter/Escape/Tab, and the combobox ARIA
|
|
25
|
-
(`combobox` on the input, `aria-expanded`, `aria-activedescendant`) — add no open/close
|
|
26
|
-
logic or ARIA roles of your own.
|
|
27
|
-
|
|
28
|
-
Needs `react-instantsearch` **7.18.0+**. If an older 7.x is pinned, upgrade within `^7`
|
|
29
|
-
rather than switching widgets.
|
|
30
|
-
|
|
31
23
|
```tsx
|
|
32
24
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
33
|
-
import { InstantSearch,
|
|
25
|
+
import { InstantSearch, SearchBox, Hits } from 'react-instantsearch'
|
|
34
26
|
import {
|
|
35
27
|
ALGOLIA_APP_ID,
|
|
36
28
|
ALGOLIA_SEARCH_API_KEY,
|
|
@@ -46,117 +38,13 @@ export function Search() {
|
|
|
46
38
|
indexName={ALGOLIA_INDEX_NAME}
|
|
47
39
|
insights={true}
|
|
48
40
|
>
|
|
49
|
-
<
|
|
50
|
-
|
|
51
|
-
indices={[
|
|
52
|
-
{
|
|
53
|
-
indexName: ALGOLIA_INDEX_NAME,
|
|
54
|
-
getURL: (item) => `/products/${item.handle}`,
|
|
55
|
-
itemComponent: ({ item, onSelect }) => (
|
|
56
|
-
<div onClick={onSelect}>{item.name}</div>
|
|
57
|
-
),
|
|
58
|
-
noResultsComponent: () => <span>No results</span>,
|
|
59
|
-
},
|
|
60
|
-
]}
|
|
61
|
-
/>
|
|
41
|
+
<SearchBox />
|
|
42
|
+
<Hits />
|
|
62
43
|
</InstantSearch>
|
|
63
44
|
)
|
|
64
45
|
}
|
|
65
46
|
```
|
|
66
47
|
|
|
67
|
-
### Navigation is `getURL`'s job, not the item component's
|
|
68
|
-
|
|
69
|
-
- `indices[].getURL` — `(item) => string`, a route that already exists; don't invent one.
|
|
70
|
-
It navigates on click **and** on Enter over a highlighted item, and records the click
|
|
71
|
-
event. Without it, Enter on a highlight does nothing.
|
|
72
|
-
- `itemComponent` renders content only: **no `<a>`, no `href`, no `onClick` beyond the
|
|
73
|
-
`onSelect` it is handed, never `stopPropagation`.** The widget's `<li>` owns the click —
|
|
74
|
-
it sends the Insights `click`, closes the panel, clears the active option. Wrapping the
|
|
75
|
-
item in a link or a router `<Link>` swallows all three while still navigating: looks
|
|
76
|
-
correct, click analytics never fire, panel stays open behind the new page.
|
|
77
|
-
- `noResultsComponent` on every index, or a query matching nothing renders an empty box.
|
|
78
|
-
- `getSearchPageURL` sends Enter with nothing highlighted to a full results page; pass it
|
|
79
|
-
only if the app has one. `.getQuery` is optional. Leave `showQuerySuggestions`,
|
|
80
|
-
`showPromptSuggestions`, `showRecent`, and `aiMode` unset — each needs an index or
|
|
81
|
-
feature (query suggestions, recent-search storage, AI chat) this project lacks.
|
|
82
|
-
- TypeScript: type records through the generic, not casts inside `itemComponent` —
|
|
83
|
-
`type ProductHit = Hit<{ title: string; handle: string }>` (`Hit` from
|
|
84
|
-
`instantsearch.js`), then `<Autocomplete<ProductHit> ... />` types `item`, `getURL`, and
|
|
85
|
-
`itemComponent`.
|
|
86
|
-
|
|
87
|
-
### Styling the panel
|
|
88
|
-
|
|
89
|
-
The widget ships no CSS. Install `instantsearch.css` and import a theme once, from the
|
|
90
|
-
component module or the app's global stylesheet:
|
|
91
|
-
|
|
92
|
-
```tsx
|
|
93
|
-
import 'instantsearch.css/themes/satellite.css'
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
The theme and your `.ais-*` overrides must be **global**: never route either through a CSS
|
|
97
|
-
Module, a scoped style block (Astro `<style>`, Vue `<style scoped>`, Svelte `<style>`), or
|
|
98
|
-
any styled-components-style wrapper that rewrites selectors. Scoping renames every
|
|
99
|
-
selector and the widget builds its DOM at runtime, so nothing matches — unstyled UI, no
|
|
100
|
-
error, build passes. Keep them in a global stylesheet, or add your own class names via
|
|
101
|
-
`classNames` and style those.
|
|
102
|
-
|
|
103
|
-
The theme ships a dark palette behind `:root[data-theme=dark], .dark`: in a dark app set
|
|
104
|
-
`data-theme="dark"` on `<html>` or render under a `.dark` ancestor, then adjust — don't
|
|
105
|
-
redefine the light palette by hand.
|
|
106
|
-
|
|
107
|
-
Palette lives in `--ais-*` custom properties on `:root` as **RGB triplets consumed through
|
|
108
|
-
`rgba()`**, not CSS colors — override with triplets or every widget color breaks:
|
|
109
|
-
|
|
110
|
-
```css
|
|
111
|
-
:root {
|
|
112
|
-
--ais-background-color-rgb: 24, 24, 27; /* not #18181b */
|
|
113
|
-
--ais-text-color-rgb: 244, 244, 245;
|
|
114
|
-
--ais-primary-color-rgb: 129, 140, 248;
|
|
115
|
-
}
|
|
116
|
-
```
|
|
117
|
-
|
|
118
|
-
Everything else is plain CSS against the widget's classes, or the `classNames` /
|
|
119
|
-
`indices[].classNames` props to add your own names to the same elements. Who owns what:
|
|
120
|
-
|
|
121
|
-
- `.ais-Autocomplete` — root. Its **container** sets the widget's width; don't size the
|
|
122
|
-
widget from the input.
|
|
123
|
-
- `.ais-AutocompleteForm` — visible frame: background, border, radius, focus ring
|
|
124
|
-
(`:focus-within`). The input's pill goes here.
|
|
125
|
-
- `.ais-AutocompleteInput` — text only; theme sets `border: 0`, `background: none`,
|
|
126
|
-
`width: 100%`. A border or width here leaves the form's frame drawn around it and
|
|
127
|
-
changes no widths.
|
|
128
|
-
- `.ais-AutocompletePanel` (+ `--open`), `.ais-AutocompletePanelLayout` — dropdown surface
|
|
129
|
-
and its scroll container.
|
|
130
|
-
- `.ais-AutocompleteIndex`, `-IndexHeader`, `-IndexList`, `-IndexItem`, `-IndexNoResults`
|
|
131
|
-
— one index's results; the active item carries `aria-selected="true"`.
|
|
132
|
-
- `.ais-AutocompleteItemWrapper`, `-ItemContent`, `-ItemIcon` — inside one result.
|
|
133
|
-
- `.ais-AutocompleteSubmitButton`, `-ClearButton`, `-LoadingIndicator`, `-Label` — form
|
|
134
|
-
controls.
|
|
135
|
-
- `.ais-AutocompleteDetached*` — detached mode (below).
|
|
136
|
-
|
|
137
|
-
Style nothing outside that list: `.ais-SearchBox-*` belongs to `SearchBox` and does
|
|
138
|
-
nothing here. Verify names against the installed version:
|
|
139
|
-
`grep -o 'ais-Autocomplete[A-Za-z-]*' node_modules/instantsearch.css/themes/satellite.css | sort -u`.
|
|
140
|
-
|
|
141
|
-
The theme's panel is `position: absolute; width: 100%`, so it inherits the input
|
|
142
|
-
container's width — a small input yields an unreadably narrow panel. Give it a min-width
|
|
143
|
-
and anchor it to the edge it opens from, or it grows off-screen:
|
|
144
|
-
|
|
145
|
-
```css
|
|
146
|
-
.ais-AutocompletePanel {
|
|
147
|
-
min-width: 320px;
|
|
148
|
-
left: 0; /* right: 0; left: auto for an input aligned to the right */
|
|
149
|
-
}
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-
Below `(max-width: 680px)` the widget goes detached (fullscreen): the inline form becomes
|
|
153
|
-
a `.ais-AutocompleteDetachedSearchButton`, form and results move into an overlay
|
|
154
|
-
(`.ais-AutocompleteDetachedOverlay`, `.ais-AutocompleteDetachedContainer`,
|
|
155
|
-
`.ais-AutocompleteDetachedFormContainer`). So panel-width rules apply only above that
|
|
156
|
-
breakpoint, and styling the inline form leaves the mobile trigger button on the theme's
|
|
157
|
-
look unless you style it too. Change it with `detachedMediaQuery` or
|
|
158
|
-
`--ais-autocomplete-detached-media-query`; pass `''` to disable detached mode entirely.
|
|
159
|
-
|
|
160
48
|
Do not inline `searchClient={algoliasearch(...)}` — keep the stable reference above.
|
|
161
49
|
|
|
162
50
|
## Insights (click analytics) — always enable it
|
|
@@ -165,28 +53,17 @@ The `insights` prop turns on the Insights middleware, which adds
|
|
|
165
53
|
`clickAnalytics: true` to every query and keeps the `userToken` in sync between search
|
|
166
54
|
and event calls. Never set `clickAnalytics` or `userToken` on `<Configure>` by hand.
|
|
167
55
|
|
|
168
|
-
Use `insights={true}
|
|
169
|
-
|
|
170
|
-
`window.aa`.
|
|
56
|
+
Use `insights={true}`. The middleware loads `search-insights` from the jsDelivr CDN
|
|
57
|
+
itself; do not install the package or pass `insightsClient`.
|
|
171
58
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
`insights={true}` is the whole implementation.
|
|
175
|
-
|
|
176
|
-
Only if the project has a Content-Security-Policy blocking jsDelivr (the CDN script then
|
|
177
|
-
cannot load): install `search-insights` from npm and pass its default export as
|
|
59
|
+
If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
|
|
60
|
+
case, install `search-insights` from npm and pass its default export as
|
|
178
61
|
`insightsClient`:
|
|
179
62
|
|
|
180
63
|
```tsx
|
|
181
64
|
import aa from 'search-insights'
|
|
182
65
|
|
|
183
|
-
|
|
184
|
-
return (
|
|
185
|
-
<InstantSearch insights={{ insightsClient: aa }}>
|
|
186
|
-
{/* widgets */}
|
|
187
|
-
</InstantSearch>
|
|
188
|
-
)
|
|
189
|
-
}
|
|
66
|
+
<InstantSearch insights={{ insightsClient: aa }} />
|
|
190
67
|
```
|
|
191
68
|
|
|
192
69
|
The middleware calls `aa('init', ...)` itself with the app ID and search key, so no
|
|
@@ -206,13 +83,8 @@ policy allows it:
|
|
|
206
83
|
|
|
207
84
|
### Click and conversion events
|
|
208
85
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
`{ item, onSelect, onApply }` and **no `sendEvent`** — nothing to wire. Call `onSelect`
|
|
212
|
-
for the widget's selection behavior (close the panel, record a recent search); the click
|
|
213
|
-
event fires either way.
|
|
214
|
-
|
|
215
|
-
`<Hits>` and `<InfiniteHits>` behave the same way. Two cases there need explicit wiring:
|
|
86
|
+
`<Hits>` and `<InfiniteHits>` send `view` and `click` events on their own. Two cases
|
|
87
|
+
need explicit wiring:
|
|
216
88
|
|
|
217
89
|
- A custom `hitComponent` whose click target is your own element — it receives a
|
|
218
90
|
`sendEvent` prop.
|
|
@@ -20,68 +20,19 @@ const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
|
20
20
|
|
|
21
21
|
## Vue (`vue-instantsearch` v4)
|
|
22
22
|
|
|
23
|
-
Use `ais-autocomplete` for the query and results state. It is **headless**: its default
|
|
24
|
-
slot provides only `currentRefinement`, `indices`, and `refine` — it renders nothing and
|
|
25
|
-
handles no focus, blur, keyboard, or ARIA. Build all of it yourself: panel closed until
|
|
26
|
-
the input is focused and has a query; close on Escape, blur, or outside click; active-option
|
|
27
|
-
highlight on Up/Down with Enter to select; combobox ARIA on the input; click events via
|
|
28
|
-
each index's `sendEvent`.
|
|
29
|
-
|
|
30
23
|
```vue
|
|
31
24
|
<template>
|
|
32
|
-
<
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
<input
|
|
41
|
-
type="search"
|
|
42
|
-
role="combobox"
|
|
43
|
-
aria-autocomplete="list"
|
|
44
|
-
aria-controls="search-panel"
|
|
45
|
-
:aria-expanded="isOpen(currentRefinement)"
|
|
46
|
-
:aria-activedescendant="
|
|
47
|
-
active >= 0 ? `search-option-${active}` : undefined
|
|
48
|
-
"
|
|
49
|
-
:value="currentRefinement"
|
|
50
|
-
@focus="open = true"
|
|
51
|
-
@input="onInput(refine, $event)"
|
|
52
|
-
@keydown.down.prevent="move(1, indices)"
|
|
53
|
-
@keydown.up.prevent="move(-1, indices)"
|
|
54
|
-
@keydown.enter.prevent="select(indices, active)"
|
|
55
|
-
@keydown.esc="close"
|
|
56
|
-
/>
|
|
57
|
-
<ul
|
|
58
|
-
v-show="isOpen(currentRefinement)"
|
|
59
|
-
id="search-panel"
|
|
60
|
-
class="search-panel"
|
|
61
|
-
role="listbox"
|
|
62
|
-
aria-label="Search results"
|
|
63
|
-
>
|
|
64
|
-
<li
|
|
65
|
-
v-for="(hit, i) in indices[0]?.hits ?? []"
|
|
66
|
-
:id="`search-option-${i}`"
|
|
67
|
-
:key="hit.objectID"
|
|
68
|
-
role="option"
|
|
69
|
-
:aria-selected="i === active"
|
|
70
|
-
:class="{ 'is-active': i === active }"
|
|
71
|
-
@mousedown.prevent="select(indices, i)"
|
|
72
|
-
@mousemove="active = i"
|
|
73
|
-
>
|
|
74
|
-
<ais-highlight attribute="name" :hit="hit" />
|
|
75
|
-
</li>
|
|
76
|
-
</ul>
|
|
77
|
-
</template>
|
|
78
|
-
</ais-autocomplete>
|
|
79
|
-
</ais-instant-search>
|
|
80
|
-
</div>
|
|
25
|
+
<ais-instant-search
|
|
26
|
+
:search-client="searchClient"
|
|
27
|
+
:index-name="ALGOLIA_INDEX_NAME"
|
|
28
|
+
:insights="true"
|
|
29
|
+
>
|
|
30
|
+
<ais-search-box />
|
|
31
|
+
<ais-hits />
|
|
32
|
+
</ais-instant-search>
|
|
81
33
|
</template>
|
|
82
34
|
|
|
83
35
|
<script setup>
|
|
84
|
-
import { ref } from 'vue'
|
|
85
36
|
import { liteClient as algoliasearch } from 'algoliasearch/lite'
|
|
86
37
|
import {
|
|
87
38
|
ALGOLIA_APP_ID,
|
|
@@ -90,71 +41,9 @@ import {
|
|
|
90
41
|
} from '<the project's shared config module>'
|
|
91
42
|
|
|
92
43
|
const searchClient = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_API_KEY)
|
|
93
|
-
const open = ref(false)
|
|
94
|
-
const active = ref(-1)
|
|
95
|
-
const containerRef = ref(null)
|
|
96
|
-
|
|
97
|
-
function isOpen(currentRefinement) {
|
|
98
|
-
return open.value && Boolean(currentRefinement)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function onInput(refine, event) {
|
|
102
|
-
open.value = true
|
|
103
|
-
active.value = -1
|
|
104
|
-
refine(event.currentTarget.value)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
function move(delta, indices) {
|
|
108
|
-
const hits = indices[0]?.hits ?? []
|
|
109
|
-
if (!hits.length) return
|
|
110
|
-
open.value = true
|
|
111
|
-
const next = active.value + delta
|
|
112
|
-
active.value = next < 0 ? hits.length - 1 : next % hits.length
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
function select(indices, index) {
|
|
116
|
-
const hit = indices[0]?.hits?.[index]
|
|
117
|
-
if (!hit) return
|
|
118
|
-
indices[0].sendEvent('click', hit, 'Hit Clicked')
|
|
119
|
-
close()
|
|
120
|
-
// TODO: navigate to this hit's page, where the app has one
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function close() {
|
|
124
|
-
open.value = false
|
|
125
|
-
active.value = -1
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
function onFocusOut(event) {
|
|
129
|
-
if (!containerRef.value?.contains(event.relatedTarget)) close()
|
|
130
|
-
}
|
|
131
44
|
</script>
|
|
132
45
|
```
|
|
133
46
|
|
|
134
|
-
Details that break accessibility if changed:
|
|
135
|
-
|
|
136
|
-
- `role="combobox"` on the `input`, not a wrapper — with `aria-controls` at the popup's
|
|
137
|
-
`id` and `aria-activedescendant` at the active option's `id`. Focus stays on the input
|
|
138
|
-
while the arrows move the highlight.
|
|
139
|
-
- The `role="listbox"` element must be the direct parent of the `role="option"` elements;
|
|
140
|
-
a `ul` inside a `listbox` div, or options one level deeper, breaks the a11y tree.
|
|
141
|
-
- `v-show`, not `v-if`, so the `aria-activedescendant` target stays in the DOM.
|
|
142
|
-
- `@mousedown.prevent` on an option, not `@click` — selection must land before `focusout`
|
|
143
|
-
closes the panel.
|
|
144
|
-
- `focusout` on the wrapper is the outside-click dismissal (`event.relatedTarget` is
|
|
145
|
-
`null` for a click on non-focusable chrome). Don't add a `document` click listener on
|
|
146
|
-
top — it fights `@mousedown.prevent`.
|
|
147
|
-
- `.is-active` needs a visible style, not just `aria-selected` — it is a keyboard user's
|
|
148
|
-
only focus feedback, since focus never leaves the input.
|
|
149
|
-
|
|
150
|
-
The panel is your own markup with no widget theme to inherit: give `.search-panel` a
|
|
151
|
-
background from the app's theme tokens (never transparent over page content), text
|
|
152
|
-
clearing 4.5:1 against it, `position: absolute` with `min-width: 320px` anchored to the
|
|
153
|
-
input edge it opens from, and a `max-height` with `overflow-y: auto`.
|
|
154
|
-
|
|
155
|
-
`indices` supports federated search across indices; this project has one, so `indices[0]`
|
|
156
|
-
is always the one to read.
|
|
157
|
-
|
|
158
47
|
Register the widgets via the InstantSearch Vue plugin in your app entry:
|
|
159
48
|
|
|
160
49
|
```js
|
|
@@ -168,15 +57,12 @@ The `insights` prop turns on the Insights middleware, which adds
|
|
|
168
57
|
`clickAnalytics: true` to every query and keeps the `userToken` in sync between search
|
|
169
58
|
and event calls. Never set `clickAnalytics` or `userToken` on `ais-configure` by hand.
|
|
170
59
|
|
|
171
|
-
Use `:insights="true"
|
|
172
|
-
|
|
60
|
+
Use `:insights="true"`. The middleware loads `search-insights` from the jsDelivr CDN itself; do not install
|
|
61
|
+
the package or pass `insightsClient`.
|
|
173
62
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
Only if the project has a Content-Security-Policy blocking jsDelivr (the CDN script then cannot
|
|
179
|
-
load): install `search-insights` from npm and pass its default export as `insightsClient`:
|
|
63
|
+
If a Content-Security-Policy blocks jsDelivr, the CDN script cannot load. In that
|
|
64
|
+
case, install `search-insights` from npm and pass its default export as
|
|
65
|
+
`insightsClient`:
|
|
180
66
|
|
|
181
67
|
```js
|
|
182
68
|
import aa from 'search-insights'
|
|
@@ -199,18 +85,19 @@ const insights = { insightsInitParams: { useCookie: true } } // then :insights="
|
|
|
199
85
|
|
|
200
86
|
### Click and conversion events
|
|
201
87
|
|
|
202
|
-
`ais-
|
|
203
|
-
|
|
204
|
-
`sendEvent` from your own option handler, as in the setup example above.
|
|
88
|
+
`ais-hits` sends `view` and `click` events on its own. A custom `item` slot whose
|
|
89
|
+
click target is your own element needs the slot's `sendEvent`:
|
|
205
90
|
|
|
206
91
|
```vue
|
|
207
|
-
<
|
|
92
|
+
<ais-hits>
|
|
93
|
+
<template v-slot:item="{ item, sendEvent }">
|
|
94
|
+
<a :href="item.url" @click="sendEvent('click', item, 'Hit Clicked')">
|
|
95
|
+
{{ item.name }}
|
|
96
|
+
</a>
|
|
97
|
+
</template>
|
|
98
|
+
</ais-hits>
|
|
208
99
|
```
|
|
209
100
|
|
|
210
|
-
`ais-hits` (if the app also has a full results page) sends both itself; only a custom
|
|
211
|
-
`item` slot whose click target is your own element needs wiring — that slot provides
|
|
212
|
-
`sendEvent`.
|
|
213
|
-
|
|
214
101
|
Conversions are business actions (add to cart, signup, booking), so wire them at the
|
|
215
102
|
real action, not at render:
|
|
216
103
|
|