diff --git a/.cspell.json b/.cspell.json index 1a636a8bb4d..d84436e0d3f 100644 --- a/.cspell.json +++ b/.cspell.json @@ -87,6 +87,7 @@ "metadatas", "Modernizr", "myapp", + "coveoua", "mycoveocloudorganizationg", "mycoveoorganization", "mycoveoorganizationg", diff --git a/packages/documentation/README.md b/packages/documentation/README.md new file mode 100644 index 00000000000..39293a97d87 --- /dev/null +++ b/packages/documentation/README.md @@ -0,0 +1,15 @@ +# @coveo/documentation + +This is a typedoc plugin. It ensures that the documentation generated adheres to our styling guidelines. + +It is used by +- `@coveo/headless` + +## Navigation +This plugin also deviates from the standard typedoc navigation patterns. It allows for top level documents that are ungrouped and it removes the standard +`Documents` tab for ungrouped documents. It also allows for groups to have a mixed of documents and folders based on Category, as opposed to forcing +each document in a group that has categories to _be_ categorized or otherwise lumped together into an `Others` folder. + +The sorting of the navigation be specified, both for the top level and optionally by for groups. + +NOTE: when specifying the sorting for groups, the key must be in lowercase not the actual casing of the document title. \ No newline at end of file diff --git a/packages/documentation/lib/formatTypeDocToolbar.ts b/packages/documentation/lib/formatTypeDocToolbar.ts index 9331f0fcddd..bb650e805bb 100644 --- a/packages/documentation/lib/formatTypeDocToolbar.ts +++ b/packages/documentation/lib/formatTypeDocToolbar.ts @@ -1,4 +1,4 @@ -export function formatTypeDocToolbar() { +export const formatTypeDocToolbar = () => { document.addEventListener('DOMContentLoaded', () => { const header = document.getElementsByTagName('header')[0] as HTMLElement; if (header) { @@ -12,4 +12,4 @@ export function formatTypeDocToolbar() { typedocThemeSelector.style.display = 'none'; } }); -} +}; diff --git a/packages/documentation/lib/hoist.ts b/packages/documentation/lib/hoist.ts new file mode 100644 index 00000000000..ec3e6a5c69d --- /dev/null +++ b/packages/documentation/lib/hoist.ts @@ -0,0 +1,78 @@ +import {normalize} from './normalize.js'; +import type {TNavNode} from './types.js'; + +/** + * Hoists any child whose title equals `fallbackCategory` so its children are promoted + * to the *parent* level, at the exact position where the bucket appeared. + * Runs depth-first so descendants are processed as well. + */ +export const hoistOtherCategoryInNav = ( + root: TNavNode, + fallbackCategory: string +) => { + if (!root) return; + + const stack: TNavNode[] = [root]; + while (stack.length) { + const node = stack.pop()!; + const kids = node.children; + if (!Array.isArray(kids) || kids.length === 0) continue; + + const nextChildren: TNavNode[] = []; + for (const child of kids) { + const title = typeof child.text === 'string' ? child.text : undefined; + if (title && normalize(title) === normalize(fallbackCategory)) { + if (Array.isArray(child.children) && child.children.length) { + // Promote grandchildren to the parent's level at this position + nextChildren.push(...child.children); + } + // Drop the bucket itself + } else { + nextChildren.push(child); + } + } + node.children = nextChildren; + + // Recurse + for (const c of node.children) stack.push(c); + } +}; + +/** + * Top-level helper for themes that return navigation as an array of nodes. + * If an element named like the fallback group exists at the root, its children are + * spliced into the root array at the same index (i.e., promoted to the top level), + * and the bucket is removed. Also applies recursive hoisting within all nodes. + */ +export const hoistOtherCategoryInArray = ( + rootItems: TNavNode[], + fallbackCategory: string, + topLevelGroup: string +) => { + if (!Array.isArray(rootItems) || rootItems.length === 0) return; + + // First pass: recursively hoist 'Other' within each item + for (const item of rootItems) { + hoistOtherCategoryInNav(item, fallbackCategory); + } + + // Second pass: hoist any top-level bucket matching either fallbackCategory ('Other') + // or the requested top-level group (e.g., 'Documents'). + let i = 0; + while (i < rootItems.length) { + const item = rootItems[i]; + const title = typeof item.text === 'string' ? item.text : undefined; + if ( + title && + (normalize(title) === normalize(fallbackCategory) || + normalize(title) === normalize(topLevelGroup)) + ) { + const replacement = Array.isArray(item.children) ? item.children : []; + // Replace the bucket node with its children (promote to top level) + rootItems.splice(i, 1, ...replacement); + // Continue at same index to handle multiple merges + continue; + } + i++; + } +}; diff --git a/packages/documentation/lib/index.tsx b/packages/documentation/lib/index.tsx index c66e8140f44..f1bb460182b 100644 --- a/packages/documentation/lib/index.tsx +++ b/packages/documentation/lib/index.tsx @@ -1,22 +1,168 @@ import {cpSync} from 'node:fs'; import {dirname, resolve} from 'node:path'; import {fileURLToPath} from 'node:url'; -// following docs https://typedoc.org/guides/development/#plugins -// eslint-disable-next-line n/no-unpublished-import -import {type Application, Converter, JSX, RendererEvent} from 'typedoc'; +import { + type Application, + Converter, + type DefaultTheme, + type DocumentReflection, + JSX, + KindRouter, + type Models, + type NavigationElement, + ParameterType, + type ProjectReflection, + RendererEvent, +} from 'typedoc'; import {formatTypeDocToolbar} from './formatTypeDocToolbar.js'; +import {hoistOtherCategoryInArray, hoistOtherCategoryInNav} from './hoist.js'; import {insertAtomicSearchBox} from './insertAtomicSearchBox.js'; import {insertBetaNote} from './insertBetaNote.js'; import {insertCustomComments} from './insertCustomComments.js'; import {insertMetaTags} from './insertMetaTags.js'; import {insertSiteHeaderBar} from './insertSiteHeaderBar.js'; +import {applyTopLevelRenameArray} from './renaming.js'; +import { + applyNestedOrderingArray, + applyNestedOrderingNode, + applyTopLevelOrderingArray, + applyTopLevelOrderingNode, +} from './sortNodes.js'; +import type {TFrontMatter, TNavNode} from './types.js'; + +class KebabRouter extends KindRouter { + // Optional: keep .html (default) or change if you want + extension = '.html'; + + protected getIdealBaseName(refl: Models.Reflection): string { + const name = refl.getFullName?.() ?? refl.name ?? ''; + if (!(refl as DocumentReflection)?.frontmatter?.slug) + return `documents/${this.getUrlSafeName(name)}`; + const {slug} = (refl as DocumentReflection).frontmatter as TFrontMatter; + + return `documents/${slug}`; + } +} const __dirname = dirname(fileURLToPath(import.meta.url)); /** * Called by TypeDoc when loaded as a plugin. */ -export function load(app: Application) { +export const load = (app: Application) => { + app.options.addDeclaration({ + name: 'hoistOther.fallbackCategory', + help: "Name of the fallback category to hoist (defaults to defaultCategory or 'Other').", + type: ParameterType.String, + }); + + app.options.addDeclaration({ + name: 'hoistOther.topLevelGroup', + help: "Name of the top-level group whose children should be promoted to root (default 'Documents').", + type: ParameterType.String, + }); + + app.options.addDeclaration({ + name: 'hoistOther.topLevelOrder', + help: 'An array to sort the top level nav by.', + type: ParameterType.Array, + }); + + app.options.addDeclaration({ + name: 'hoistOther.nestedOrder', + help: "Object mapping parent title -> ordering array for its children. Use '*' for a default. If omitted, children are sorted alphabetically.", + type: ParameterType.Mixed, + }); + + app.options.addDeclaration({ + name: 'hoistOther.renameModulesTo', + help: "If set, rename any top-level group titled 'Modules' to this string.", + type: ParameterType.String, + }); + + const originalMethodName = 'getNavigation'; + let originalMethod: ( + project: ProjectReflection + ) => NavigationElement[] | null = null; + app.renderer.on('beginRender', () => { + const theme = app.renderer.theme as DefaultTheme | undefined; + if (!theme) return; + + originalMethod = theme.getNavigation; + + if (!originalMethod) return; + + const opts = app.options; + const fallback = + (opts.getValue('hoistOther.fallbackCategory') as string) || + (opts.getValue('defaultCategory') as string) || + 'Other'; + + const topLevelGroup = + (opts.getValue('hoistOther.topLevelGroup') as string) || 'Documents'; + + const topLevelOrder = + (opts.getValue('hoistOther.topLevelOrder') as string[] | undefined) || + undefined; + + let nestedOrder = opts.getValue('hoistOther.nestedOrder') as + | Record + | string + | undefined; + if (typeof nestedOrder === 'string') { + try { + nestedOrder = JSON.parse(nestedOrder); + } catch {} + } + + const renameModulesTo = + (opts.getValue('hoistOther.renameModulesTo') as string | undefined) || + undefined; + + const typedNestedOrder = nestedOrder as Record; + + theme.getNavigation = function wrappedNavigation( + this: unknown, + ...args: unknown[] + ) { + const nav = originalMethod!.apply(this, args); + + // The nav shape can be an array of nodes or a single root with children + if (Array.isArray(nav)) { + if (renameModulesTo?.trim()) { + applyTopLevelRenameArray(nav, 'Modules', renameModulesTo.trim()); + } + + hoistOtherCategoryInArray(nav as TNavNode[], fallback, topLevelGroup); + + if (topLevelOrder?.length) { + applyTopLevelOrderingArray(nav as TNavNode[], topLevelOrder); + } + + applyNestedOrderingArray(nav as TNavNode[], typedNestedOrder); + } else if (nav && typeof nav === 'object') { + if (renameModulesTo?.trim() && Array.isArray(nav.children)) { + applyTopLevelRenameArray( + nav.children, + 'Modules', + renameModulesTo.trim() + ); + } + + hoistOtherCategoryInNav(nav as TNavNode, fallback); + if ( + (nav as TNavNode).children && + topLevelOrder && + topLevelOrder.length + ) { + applyTopLevelOrderingNode(nav as TNavNode, topLevelOrder); + } + applyNestedOrderingNode(nav as TNavNode, typedNestedOrder); + } + return nav; + }; + }); + // Need the Meta Tags to be inserted first, or it causes issues with the navigation sidebar app.renderer.hooks.on('head.begin', () => ( <> @@ -119,14 +265,8 @@ export function load(app: Application) { )); - const baseAssetsPath = '../../documentation/assets'; - - const createFileCopyEntry = (sourcePath: string) => ({ - from: resolve(__dirname, `${baseAssetsPath}/${sourcePath}`), - to: resolve(app.options.getValue('out'), `assets/${sourcePath}`), - }); - - const onRenderEnd = () => { + app.renderer.on(RendererEvent.END, () => { + const baseAssetsPath = '../../documentation/assets'; const filesToCopy = [ 'css/docs-style.css', 'css/main-new.css', @@ -145,7 +285,10 @@ export function load(app: Application) { ]; filesToCopy.forEach((filePath) => { - const file = createFileCopyEntry(filePath); + const file = { + from: resolve(__dirname, `${baseAssetsPath}/${filePath}`), + to: resolve(app.options.getValue('out'), `assets/${filePath}`), + }; cpSync(file.from, file.to); }); @@ -153,11 +296,17 @@ export function load(app: Application) { from: resolve(__dirname, '../../documentation/dist/dark-mode.js'), to: resolve(app.options.getValue('out'), 'assets/vars/dark-mode.js'), }; + // Restore original to avoid side effects + const theme = app.renderer.theme as DefaultTheme | undefined; + if (theme && originalMethodName && originalMethod) { + theme[originalMethodName] = originalMethod; + } + originalMethod = null; cpSync(darkModeJs.from, darkModeJs.to); - }; + }); - app.renderer.on(RendererEvent.END, onRenderEnd); + app.renderer.defineRouter('kebab', KebabRouter); app.converter.on(Converter.EVENT_CREATE_DECLARATION, insertCustomComments); -} +}; diff --git a/packages/documentation/lib/insertAtomicSearchBox.ts b/packages/documentation/lib/insertAtomicSearchBox.ts index b4951570c97..e447f906a09 100644 --- a/packages/documentation/lib/insertAtomicSearchBox.ts +++ b/packages/documentation/lib/insertAtomicSearchBox.ts @@ -13,7 +13,7 @@ declare global { } } -export function insertAtomicSearchBox() { +export const insertAtomicSearchBox = () => { const areFunctionalCookiesEnabled = (): boolean => { return document.cookie .split('; ') @@ -54,4 +54,4 @@ export function insertAtomicSearchBox() { })(); } }); -} +}; diff --git a/packages/documentation/lib/insertBetaNote.ts b/packages/documentation/lib/insertBetaNote.ts index 85558ff3623..2c292cd9a35 100644 --- a/packages/documentation/lib/insertBetaNote.ts +++ b/packages/documentation/lib/insertBetaNote.ts @@ -1,4 +1,4 @@ -export function insertBetaNote() { +export const insertBetaNote = () => { document.addEventListener('DOMContentLoaded', () => { const breadcrumbs = document.querySelector('ul.tsd-breadcrumb'); if (breadcrumbs) { @@ -14,4 +14,4 @@ export function insertBetaNote() { } } }); -} +}; diff --git a/packages/documentation/lib/insertCoveoLogo.ts b/packages/documentation/lib/insertCoveoLogo.ts index fc150904665..6b900177012 100644 --- a/packages/documentation/lib/insertCoveoLogo.ts +++ b/packages/documentation/lib/insertCoveoLogo.ts @@ -1,4 +1,4 @@ -export function insertCoveoLogo(imagePath: string) { +export const insertCoveoLogo = (imagePath: string) => { document.addEventListener('DOMContentLoaded', () => { const toolbarContents = document.getElementsByClassName( 'tsd-toolbar-contents' @@ -24,4 +24,4 @@ export function insertCoveoLogo(imagePath: string) { faviconLink.rel = 'icon'; faviconLink.href = `${imagePath}/favicon.ico`; document.head.appendChild(faviconLink); -} +}; diff --git a/packages/documentation/lib/insertCustomComments.ts b/packages/documentation/lib/insertCustomComments.ts index 050015ea7b5..c35aac56123 100644 --- a/packages/documentation/lib/insertCustomComments.ts +++ b/packages/documentation/lib/insertCustomComments.ts @@ -17,6 +17,7 @@ const comments = [ }, ]; +// NOTE: cannot be converted into an arrow function `this` export function insertCustomComments( this: undefined, _ctx: Context, diff --git a/packages/documentation/lib/insertMetaTags.ts b/packages/documentation/lib/insertMetaTags.ts index f70183c93e7..7190a62feb2 100644 --- a/packages/documentation/lib/insertMetaTags.ts +++ b/packages/documentation/lib/insertMetaTags.ts @@ -1,4 +1,4 @@ -export function insertMetaTags() { +export const insertMetaTags = () => { const head = document.getElementsByTagName('head')[0]; if (head) { head.innerHTML += ` @@ -8,4 +8,4 @@ export function insertMetaTags() { `; } -} +}; diff --git a/packages/documentation/lib/insertSiteHeaderBar.ts b/packages/documentation/lib/insertSiteHeaderBar.ts index 12d34cfb506..93033ef11b5 100644 --- a/packages/documentation/lib/insertSiteHeaderBar.ts +++ b/packages/documentation/lib/insertSiteHeaderBar.ts @@ -1,4 +1,4 @@ -export function insertSiteHeaderBar(assetsPath: string) { +export const insertSiteHeaderBar = (assetsPath: string) => { const isDefaultUserDarkTheme = window.matchMedia('(prefers-color-scheme: dark)').matches || window.matchMedia('(prefers-color-scheme:dark)').matches; @@ -91,4 +91,4 @@ export function insertSiteHeaderBar(assetsPath: string) { document.head.appendChild(faviconLink); } }); -} +}; diff --git a/packages/documentation/lib/insertSurveyLink.ts b/packages/documentation/lib/insertSurveyLink.ts index 152146a1019..735f5d3949a 100644 --- a/packages/documentation/lib/insertSurveyLink.ts +++ b/packages/documentation/lib/insertSurveyLink.ts @@ -1,4 +1,4 @@ -export function insertSurveyLink() { +export const insertSurveyLink = () => { document.addEventListener('DOMContentLoaded', () => { const toolbarWidgets = document.getElementById('tsd-widgets'); if (toolbarWidgets) { @@ -13,4 +13,4 @@ export function insertSurveyLink() { toolbarWidgets.appendChild(feedbackDiv); } }); -} +}; diff --git a/packages/documentation/lib/normalize.ts b/packages/documentation/lib/normalize.ts new file mode 100644 index 00000000000..3c1a8d351a5 --- /dev/null +++ b/packages/documentation/lib/normalize.ts @@ -0,0 +1,3 @@ +export const normalize = (s: string) => { + return s.trim().toLowerCase(); +}; diff --git a/packages/documentation/lib/renaming.ts b/packages/documentation/lib/renaming.ts new file mode 100644 index 00000000000..07adde6c1fd --- /dev/null +++ b/packages/documentation/lib/renaming.ts @@ -0,0 +1,15 @@ +import {normalize} from './normalize.js'; +import type {TNavNode} from './types.js'; + +export const applyTopLevelRenameArray = ( + items: TNavNode[], + from: string, + to: string +) => { + if (!Array.isArray(items) || items.length === 0) return; + const fromN = normalize(from); + for (const item of items) { + const t = typeof item.text === 'string' ? item.text : undefined; + if (t && normalize(t) === fromN) item.text = to; + } +}; diff --git a/packages/documentation/lib/sortNodes.ts b/packages/documentation/lib/sortNodes.ts new file mode 100644 index 00000000000..9c6990750c4 --- /dev/null +++ b/packages/documentation/lib/sortNodes.ts @@ -0,0 +1,76 @@ +import {normalize} from './normalize.js'; +import type {TNavNode} from './types.js'; + +// Top-level & nested ordering utilities +export const applyTopLevelOrderingArray = ( + items: TNavNode[], + order: string[] +) => { + if (!Array.isArray(items) || items.length === 0 || !order?.length) return; + const spec = order.map((s) => normalize(s)); + const wildcard = spec.indexOf('*'); + + items.sort((a, b) => rankCompare(a, b, spec, wildcard)); +}; + +export const applyTopLevelOrderingNode = (root: TNavNode, order: string[]) => { + if (!root?.children?.length) return; + applyTopLevelOrderingArray(root.children, order); +}; + +// Nested ordering: if `order` is provided, apply rank-based ordering at every level; +// otherwise sort alphabetically by `text`. Always recurse into children. +export const applyNestedOrderingNode = ( + root: TNavNode, + orderMap?: Record, + keyPrefix?: string +) => { + if (!root) return; + if (Array.isArray(root.children) && root.children.length) { + const key = [keyPrefix, normalize(String(root.text ?? ''))] + .join(' ') + .trim(); + const spec = orderMap && (orderMap[key] || orderMap['*']); + if (spec?.length) applyOrderingArray(root.children, spec); + else root.children.sort(alphaByText); + for (const c of root.children) applyNestedOrderingNode(c, orderMap, key); + } +}; + +export const applyNestedOrderingArray = ( + items: TNavNode[], + orderMap?: Record +) => { + if (!Array.isArray(items) || items.length === 0) return; + for (const item of items) applyNestedOrderingNode(item, orderMap); +}; + +const applyOrderingArray = (items: TNavNode[], order: string[]) => { + const spec = order.map((s) => normalize(s)); + const wildcard = spec.indexOf('*'); + items.sort((a, b) => rankCompare(a, b, spec, wildcard)); +}; + +const rankCompare = ( + a: TNavNode, + b: TNavNode, + spec: string[], + wildcardIdx: number +) => { + const an = normalize(String(a.text ?? '')); + const bn = normalize(String(b.text ?? '')); + const ar = spec.indexOf(an); + const br = spec.indexOf(bn); + + const aRank = ar >= 0 ? ar : wildcardIdx >= 0 ? wildcardIdx : spec.length; + const bRank = br >= 0 ? br : wildcardIdx >= 0 ? wildcardIdx : spec.length; + + if (aRank !== bRank) return aRank - bRank; + return an.localeCompare(bn); +}; + +const alphaByText = (a: TNavNode, b: TNavNode) => { + return normalize(String(a.text ?? '')).localeCompare( + normalize(String(b.text ?? '')) + ); +}; diff --git a/packages/documentation/lib/types.ts b/packages/documentation/lib/types.ts new file mode 100644 index 00000000000..c756962667a --- /dev/null +++ b/packages/documentation/lib/types.ts @@ -0,0 +1,15 @@ +export type TNavNode = { + text?: string; + path?: string | null; + children?: TNavNode[] | undefined; + class?: string; + icon?: string | number; + [key: string]: unknown; +}; + +export type TFrontMatter = { + group?: string; + category?: string; + title?: string; + slug?: string; +}; diff --git a/packages/headless-react/COMMERCEREADME.md b/packages/headless-react/COMMERCEREADME.md index de10cfbc80a..fd156a492d4 100644 --- a/packages/headless-react/COMMERCEREADME.md +++ b/packages/headless-react/COMMERCEREADME.md @@ -1,3 +1,6 @@ +--- +title: Introduction +--- # Headless React Utils for SSR (Open Beta) `@coveo/headless-react/ssr-commerce` is currently in Open Beta and provides React utilities for server-side rendering with Headless Commerce controllers. diff --git a/packages/headless-react/README.md b/packages/headless-react/README.md index c06240c1733..b7df91b6a7b 100644 --- a/packages/headless-react/README.md +++ b/packages/headless-react/README.md @@ -1,3 +1,6 @@ +--- +title: Home +--- # Headless React Utils for SSR diff --git a/packages/headless-react/SEARCHREADME.md b/packages/headless-react/SEARCHREADME.md index 133ccd0e3e2..28fd9d9bf3d 100644 --- a/packages/headless-react/SEARCHREADME.md +++ b/packages/headless-react/SEARCHREADME.md @@ -1,3 +1,6 @@ +--- +title: Introduction +--- # Headless React Utils for SSR `@coveo/headless-react/ssr` provides React utilities for server-side rendering with headless controllers. diff --git a/packages/headless-react/documents/headless-react/hooks.md b/packages/headless-react/documents/headless-react/hooks.md index 520a1566003..37e6aa6bfa4 100644 --- a/packages/headless-react/documents/headless-react/hooks.md +++ b/packages/headless-react/documents/headless-react/hooks.md @@ -1,5 +1,5 @@ --- -title: Controller hooks +title: Commerce Controller hooks --- # Hooks diff --git a/packages/headless-react/typedoc-configs/ssr-commerce.typedoc.json b/packages/headless-react/typedoc-configs/ssr-commerce.typedoc.json index 12ef40dccff..b6a1d5d645d 100644 --- a/packages/headless-react/typedoc-configs/ssr-commerce.typedoc.json +++ b/packages/headless-react/typedoc-configs/ssr-commerce.typedoc.json @@ -5,20 +5,8 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Providers", - "Definers", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], "name": "SSR Commerce", - "readme": "../COMMERCEREADME.md", "entryPoints": ["../src/ssr-commerce/index.ts"], "gitRevision": "main", - "plugin": ["@coveo/documentation"], - "projectDocuments": ["../documents/headless-react/*.md"] + "plugin": ["@coveo/documentation"] } diff --git a/packages/headless-react/typedoc-configs/ssr.typedoc.json b/packages/headless-react/typedoc-configs/ssr.typedoc.json index edeb376cc5e..f0502eda119 100644 --- a/packages/headless-react/typedoc-configs/ssr.typedoc.json +++ b/packages/headless-react/typedoc-configs/ssr.typedoc.json @@ -5,17 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Definers", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], "name": "SSR Search", - "readme": "../SEARCHREADME.md", "entryPoints": ["../src/ssr/index.ts"], "gitRevision": "main", "plugin": ["@coveo/documentation"] diff --git a/packages/headless-react/typedoc.json b/packages/headless-react/typedoc.json index 86dc286859f..1f124632b45 100644 --- a/packages/headless-react/typedoc.json +++ b/packages/headless-react/typedoc.json @@ -1,13 +1,42 @@ { "entryPointStrategy": "merge", + "projectDocuments": ["documents/headless-react/hooks.md", "README.md"], "navigation": { "includeCategories": true, "includeGroups": true, "includeFolders": true }, + "hoistOther.topLevelOrder": [ + "Home", + "Usage", + "Commerce Controller hooks", + "Reference" + ], "categorizeByGroup": true, "entryPoints": ["./temp/ssr-commerce.json", "./temp/ssr.json"], "gitRevision": "main", "plugin": ["@coveo/documentation"], - "projectDocuments": ["./README.md"] + "hoistOther.renameModulesTo": "Reference", + "router": "kebab", + "hoistOther.nestedOrder": { + "reference ssr commerce": [ + "Engine", + "Providers", + "Definers", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ], + "reference ssr search": [ + "Engine", + "Definers", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ] + } } diff --git a/packages/headless/README.md b/packages/headless/README.md index 340b24a72db..4ee53b86aa3 100644 --- a/packages/headless/README.md +++ b/packages/headless/README.md @@ -1,3 +1,7 @@ +--- +title: Contributing +--- + [![npm version](https://badge.fury.io/js/@coveo%2Fheadless.svg)](https://badge.fury.io/js/@coveo%2Fheadless) # Coveo Headless diff --git a/packages/headless/source_docs/coveo-headless-home.md b/packages/headless/source_docs/coveo-headless-home.md new file mode 100644 index 00000000000..b6c109233e4 --- /dev/null +++ b/packages/headless/source_docs/coveo-headless-home.md @@ -0,0 +1,63 @@ +--- +title: Home +slug: index +--- +# Use the Headless library + +_Coveo Headless_ is a library for developing Coveo-powered UI components. +It works as a middle layer for applications, opening a line of communication between the UI elements and the [Coveo Platform](https://docs.coveo.com/en/186/). + +For example, the [Coveo Atomic](https://docs.coveo.com/en/atomic/latest/) library relies on Headless to handle interactions between the application state and Coveo. Platform. + +> [!NOTE] +> Coveo also provide a version of the Headless Library for use with React Projects. +> See the [Headless-React reference documentation](https://docs.coveo.com/en/headless-react/latest/reference/index.html). + +At its core, Headless consists of an _engine_ whose main property is its _state_ (that is, a [Redux store](https://redux.js.org/api/store)). +The engine’s state depends on a set of _features_ (that is, [reducers](https://redux.js.org/basics/reducers)). +To interact with these features, Headless provides a collection of _controllers_. + +For example, to implement a search box UI component, you would use the Headless search box controller. +This exposes various methods, such as `updateText`, `submit`, and `showSuggestion`. + +Under the hood, Headless relies on different Coveo APIs depending on your solution: + +* For sending analytics data, Headless uses the Coveo Event API. +* For non-commerce solutions, Headless interacts with the Coveo Platform using the Coveo Search API. + + For Coveo for Commerce solutions, Headless interacts with the Coveo Platform using the Coveo Commerce API. + For more details, see the documentation on the [commerce engine](https://docs.coveo.com/en/o52e9091/). + +![Diagram showing where Headless fits with Atomic and the APIs](https://docs.coveo.com/en/assets/images/headless/headless.svg) + +## When Should I Use Headless? + +The Headless library wraps the complexity of the Coveo APIs without sacrificing flexibility. +It’s usable with any web development framework and it manages the state of your search page for you. +Rather than prebuilt UI components, it provides an extendable set of reducers and controllers that allow you to connect your own search UI components to the Coveo APIs. + +If you want to use Coveo to power your own UI component library, then you should definitely consider using Headless. +It’s the easiest and least error-prone approach to developing and maintaining your Coveo-powered UI component library. + +The following interactive code sample uses Coveo Headless alongside the [Material-UI React framework](https://material-ui.com/) to create a simple search page. + + + +
💡 TIP: Leading practice
+ +Unless you need full control over the appearance of your page, Headless is most likely not for you. +Rather, to quickly assemble a feature-rich search interface, consider using Coveo Atomic, our prebuilt, modern component library. +
+ +Additionally, in rare cases you may need to develop directly against the Coveo APIs, such as when you want to integrate Coveo search features inside a non-web-based application. + +## Where Do I Start? + +To learn the basics of the Headless library, see the [Usage](https://docs.coveo.com/en/headless/latest/usage) and [Reference](https://docs.coveo.com/en/headless/latest/reference) sections. + +To create a starter Angular, React, or Vue.js project with a Coveo Headless-powered search page, check out the [Coveo CLI](https://github.com/coveo/cli#readme). \ No newline at end of file diff --git a/packages/headless/source_docs/coveo-headless-usage.md b/packages/headless/source_docs/coveo-headless-usage.md new file mode 100644 index 00000000000..fdc60227809 --- /dev/null +++ b/packages/headless/source_docs/coveo-headless-usage.md @@ -0,0 +1,304 @@ +--- +title: Introduction +group: Usage +slug: usage/index +--- +# Usage + +A project built on top of Headless will typically involve two main building-blocks: the _engine,_ which manages the state of the search interface and communicates with the Coveo Platform, and the _controllers,_ which dispatch actions to the engine based on user interactions. + +
📌 Note
+ +To create a starter Angular, React, or Vue.js project with a Coveo Headless-powered search page, check out the [Coveo CLI](https://github.com/coveo/cli#readme). +The CLI can handle several steps for you. +
+ +This article provides an overview of the core Headless concepts. + +## Install Headless + +Use [npm](https://www.npmjs.com/get-npm) to install the Headless library. + +``` +npm install @coveo/headless +``` + +Headless requires Node.js version 20. + +
📌 Note
+ +If you use TypeScript, note that Headless doesn’t support the `classic` or `node10`/`node` `moduleResolution` options. +See [TypeScript module resolution](https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution) and [Announcing TypeScript 5.0 `--moduleResolution bundler`](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#--moduleresolution-bundler). +
+ +## Configure a Headless Engine + +To start building an application on top of the [Headless library](https://docs.coveo.com/en/lcdf0493/), you must first initialize a [Headless engine](https://docs.coveo.com/en/headless/latest/reference/index.html) using one of the builder functions, each of which is dedicated to a specific use case. +Each builder function is imported from a dedicated sub-package that has the relevant exports for its use case. +These engine builder functions are: + +* `buildSearchEngine` (imported from `@coveo/headless'`) +* `buildCaseAssistEngine` (imported from `@coveo/headless/case-assist'`) +* [`buildCommerceEngine`](https://docs.coveo.com/en/o52e9091/) (imported from `@coveo/headless/commerce'`) +* `buildInsightEngine` (imported from `@coveo/headless/insight'`) +* `buildRecommendationEngine` (imported from `@coveo/headless/recommendation'`) +* `defineSearchEngine` (imported from `@coveo/headless/ssr'`) + +You’ll specify your _search endpoint_ configuration through this instance (that is, where to send Coveo search requests and how to [authenticate](https://docs.coveo.com/en/2120/) them). + +For testing purposes, you can pass the sample configuration for the engine builder function that you’re calling: + +```ts +// app/Engine.ts + +import { buildSearchEngine, getSampleSearchEngineConfiguration } from '@coveo/headless'; + +// If you're using a different engine builder function, this would be something like the following: +// import {buildRecommendationEngine, getSampleRecommendationEngineConfiguration} from '@coveo/headless/recommendation'; + +export const headlessEngine = buildSearchEngine({ + configuration: getSampleSearchEngineConfiguration() +}); + +// If you're using a different engine builder function, this would be something like the following: +// export const recommendationEngine = buildRecommendationEngine({ +// configuration: getSampleRecommendationEngineConfiguration() +// }); +``` + +However, most of the time, your initialization and export will look like this: + +```ts +// app/Engine.ts + +import { buildSearchEngine } from '@coveo/headless'; + +export const headlessEngine = buildSearchEngine({ + configuration: { + organizationId: '', ① + accessToken: '', ② + renewAccessToken: , ③ + } +}); +``` + +1. `` (string) is the [unique identifier of your Coveo organization](https://docs.coveo.com/en/n1ce5273/) (for example, `mycoveoorganization`). +2. `` (string) is an [API key](https://docs.coveo.com/en/105/) that was created using the **Anonymous search** [template](https://docs.coveo.com/en/1718#api-key-templates) or a [search token](https://docs.coveo.com/en/56/) that grants the **Allowed** [access level](https://docs.coveo.com/en/2818/) on the [**Execute Queries**](https://docs.coveo.com/en/1707#execute-queries-domain) [domain](https://docs.coveo.com/en/2819/) and the **Push** [access level](https://docs.coveo.com/en/2818/) on the [**Analytics Data**](https://docs.coveo.com/en/1707#administrate-domain) [domain](https://docs.coveo.com/en/2819/) in the target [organization](https://docs.coveo.com/en/185/). +3. `` (function) returns a new access token, usually by fetching it from a backend service that can generate [search tokens](https://docs.coveo.com/en/56/). +The engine will automatically run this function when the current access token expires (that is, when the engine detects a `419 Authentication Timeout` HTTP code). + +
📌 Note
+ + You don’t need to specify a `renewAccessToken` callback if your application is using [API key authentication](https://docs.coveo.com/en/105/). + This is typically not recommended, but can be legitimate in some scenarios. +
+ +## Use Headless Controllers + +A Headless _controller_ is an abstraction that simplifies the implementation of a specific Coveo-powered UI feature or component. +In other words, controllers provide intuitive programming interfaces for interacting with the Headless engine’s state. + +For most use cases, we recommend that you use controllers to interact with the state. + +**Example** + +To implement a search box UI component, you decide to use the [`SearchBox`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchBox.html) controller, available in the Headless [Search Engine](https://docs.coveo.com/en/headless/latest/reference/modules/Search.html). + +This controller exposes various public methods such as `updateText` and `submit`. +You write code to ensure that when the end user types in the search box, the `Searchbox` controller’s `updateText` method is called. +Under the hood, calling this method dispatches some actions. +The Headless Search Engine’s reducers react to those actions by updating the state as needed. + +In this case, the `query` property of the state is updated. +Then, the controller fetches new query suggestions from the Search API, and updates the `querySuggestState` property. + +For detailed information on the various Headless controllers, see the [reference documentation](https://docs.coveo.com/en/headless/latest/reference/index.html). + +### Initialize a Controller Instance + +You can initialize a Headless Controller instance by calling its builder function. + +A controller’s builder function always requires a Headless engine instance as a first argument. + +```typescript +// src/Components/MySearchBox.ts +  +import { SearchBox, buildSearchBox } from '@coveo/headless'; +import { engine } from '../Engine'; +  +const mySearchBox: SearchBox = buildSearchBox(engine); +``` + +Many builder functions also accept, and sometimes require, an options object as a second argument. + +```typescript +// src/Components/MyStandaloneSearchBox.ts +  +import { SearchBox, SearchBoxOptions, buildSearchBox } from '@coveo/headless'; +import { engine } from '../Engine'; +  +const options: SearchBoxOptions = { ① + numberOfSuggestions: 3, + isStandalone: true +} +  +const myStandaloneSearchBox: SearchBox = buildSearchBox(engine, { options }); +``` +1. None of the `SearchBox` controller options are required, but you can use them to tailor the controller instance to your needs. + +```typescript +// src/Components/MyAuthorFacet.ts +  +import { Facet, FacetOptions, buildFacet } from '@coveo/headless'; +import { engine } from '../Engine'; +  +const options: FacetOptions = { field: "author" }; ① +  +const myAuthorFacet: Facet = buildFacet(engine, { options }); +``` +1. Specifying a `field` value in the options object is required to initialize a `Facet` controller instance. + +The options that are available on each controller are detailed in the [reference documentation](https://docs.coveo.com/en/headless/latest/reference/index.html). + +### Interact With a Controller + +The different Headless engines expose different controllers, and each controller exposes a public interface which you can use to interact with its Headless engine’s state. + +When you call a method on a controller instance, one or more actions are dispatched. The target Headless engine’s reducers listen to those actions, and react to them by altering the state as necessary. + +Calling some of the `SearchBox` controller’s methods. + +```typescript +import { SearchBox, buildSearchBox } from '@coveo/headless'; +import { engine } from '../Engine'; + +const mySearchBox: SearchBox = buildSearchBox(engine); +  +mySearchBox.updateText('hello'); ① +  +mySearchBox.selectSuggestion(mySearchBox.state.suggestions[0].value) ② +``` +1. Dispatches actions to update the query to `hello`, and fetch new query suggestions. +2. Dispatches actions to set the query to the value of the first query suggestion (for example, `hello world`), and execute that query. + +The methods that are available on each controller are detailed in the [reference documentation](https://docs.coveo.com/en/headless/latest/reference/index.html). + +### Subscribe to State Changes + +You can use the `subscribe` method on a controller instance to listen to its state changes. + +The listener function you pass when calling the `subscribe` method will be executed every time an action is dispatched. + +```typescript +import { SearchBox, buildSearchBox } from '@coveo/headless'; +import { engine } from '../Engine'; + +const mySearchBox: SearchBox = buildSearchBox(engine); +  +// ... +  +function onSearchBoxUpdate() { + const state = mySearchBox.state; + // ...Do something with the updated SearchBox state... +} +  +const unsubscribe = mySearchBox.subscribe(onSearchBoxUpdate); ① +  +mySearchBox.updateText('hello'); ② +  +// ... +  +unsubscribe(); ③ +``` +1. Every time the portion of the state that’s relevant for the `SearchBox` controller changes, the `onSearchBoxUpdate` function will be called. +2. This will trigger a `SearchBox` state change. +3. The `subscribe` method returns an `unsubscribe` method which you can call to stop listening for state updates (for example, when the controller is deleted). + +You can also call the `subscribe` method from your Headless engine instance to listen to all state changes. + +```typescript +import { engine } from '../Engine'; +  +function onStateUpdate() { + const state = engine.state; + // ...Do something with the updated state... +} +  +const unsubscribe = engine.subscribe(onStateUpdate); +  +// ... +  +unsubscribe(); +``` + +## Initialize Your Interface + +Once you’ve initialized your engine and controllers, you may want to set initial search parameters, if needed, before [synchronizing the search parameters with values retrieved from the URL](https://docs.coveo.com/en/headless/latest/usage/synchronize-search-parameters-with-the-url) and finally triggering the first request. +If your application does not need to modify initial search parameters, then the next step would be to trigger the first request. + +In any case, be sure to modify initial search parameters, and then to synchronize with the URL, only after the engine and controllers have been initialized, lest you face timing problems. +To avoid this kind of issue, modern frameworks expose purpose-built lifecycle methods. +In React, there is [`componentDidMount`](https://reactjs.org/docs/react-component.html#componentdidmount), in Vue.js, there is [`mounted`](https://v3.vuejs.org/api/options-lifecycle-hooks.html#mounted), and in Angular there is [`ngAfterViewInit`](https://angular.io/guide/lifecycle-hooks#lifecycle-event-sequence). + +```typescript +// app/SearchPage.tsx + +import { headlessEngine } from './Engine'; ① +import { mySearchBox } from './Components/MySearchBox.ts'; ② +import { myAuthorFacet } from './Components/MyAuthorFacet.ts'; +// ... +  +export default class App extends React.Component { +  + render() { + return ( +
+ + ③ + + + +
+ ) + } +  + componentDidMount() { + this.engine.executeFirstSearch(); ④ + } +  + // ... +} +``` + +1. Import the engine [you created above](https://docs.coveo.com/en/headless/latest/usage#configure-a-headless-engine). +2. Import the search box and author facet [you created above](https://docs.coveo.com/en/headless/latest/usage#initialize-a-controller-instance). +Recall that you pass the engine to each of those components. +3. Use your components to build your interface. +4. The `componentDidMount` method allows you to wait until the `mySearchBox` and `myAuthorFacet` components have been initialized and have registered their state on the engine. +You can therefore execute the first search. +If you wanted to modify search parameters and to [synchronize them with values retrieved from the URL](https://docs.coveo.com/en/headless/latest/usage/synchronize-search-parameters-with-the-url), you would do it in this block, before executing the first search. + +## Dispatch Actions + +You’ll often use controller methods to interact with the state. However, you may sometimes want to dispatch actions on your own. You can create actions using action loaders and dispatch them using the Headless engine’s `dispatch` method. + +```typescript +import { engine } from './engine'; +import { loadFieldActions } from '@coveo/headless'; +  +const FieldActionCreators = loadFieldActions(engine); ① +const action = FieldActionCreators.registerFieldsToInclude(['field1', 'field2']); ② +  +engine.dispatch(action); ③ +``` +1. The action loader `loadFieldActions` allows you to use field actions. Calling this function will add the necessary reducers to the engine, if they haven’t been added already, and return an object holding the relevant action creator functions. +2. To create a dispatchable action, use the action creators that were loaded in the previous line. In this case, the `registerFieldsToInclude` method takes field names as parameters and returns an action that, when dispatched, will cause those fields to be returned as part of each search result. +3. Dispatch the action using the engine’s `dispatch` method. + +
📌 Note
+ +Every action dispatch triggers the corresponding listener function. +Consequently, you should only perform costly operations, such as rendering a UI component, when the state they depend on has changed. +
+ +For more information on the various actions you can dispatch, see the [reference documentation](https://docs.coveo.com/en/headless/latest/reference/index.html). \ No newline at end of file diff --git a/packages/headless/source_docs/dependent-facets.md b/packages/headless/source_docs/dependent-facets.md new file mode 100644 index 00000000000..3fb75d4e553 --- /dev/null +++ b/packages/headless/source_docs/dependent-facets.md @@ -0,0 +1,89 @@ +--- +title: Use dependent facets +group: Usage +slug: usage/use-dependent-facets +--- +# Use dependent facets + +In the context of a search application, a frequent use case is to define relationships between facets (or filters), so that a dependent facet will only appear when the user interacts with its specified parent facet. + +Let’s assume that you have a `@moviegenre` facet field in your Coveo index, as well as a `@moviesubgenre` facet field. + +`@moviegenre` may contain values such as `Action`, `Comedy`, or `Drama`. + +`@moviesubgenre` may contain values such as `War and Military Action`, `Martial Arts Action`, `Parody Comedy`, or `Historical Drama`. + +In your search interface, you only want the user to see the **Movie genre** facet initially. +The **Movie subgenre** facet only appears after a selection is made in the **Movie genre** facet. + +## Defining the Relationship + +The facet conditions manager lets you define dependencies to use when enabling a facet. + +```typescript +import { buildFacetConditionsManager } from "@coveo/headless"; +import type { + SearchEngine, + Facet, + AnyFacetValuesCondition, + FacetValueRequest, +} from "@coveo/headless"; + +function makeDependent( + engine: SearchEngine, + dependentFacet: Facet, + parentFacets: Facet[] +) { + const facetConditionsManager = buildFacetConditionsManager(engine, { + facetId: dependentFacet.state.facetId, + conditions: [ + parentFacets.map((parentFacet) => { + const parentFacetHasAnySelectedValueCondition: AnyFacetValuesCondition = + { + parentFacetId: parentFacet.state.facetId, + condition: (parentValues) => + parentValues.some((v) => v.state === "selected"), + }; + return parentFacetHasAnySelectedValueCondition; + }), + ], + }); + + dependentFacet.subscribe(() => { + setFacetVisibility( + dependentFacet.state.facetId, + dependentFacet.state.enabled + ); + }); + + addOnFacetDestroyedListener(dependentFacet.state.facetId, () => { + facetConditionsManager.stopWatching(); + }); +} + +/** + * Show or hide the facet from the user. + */ +function setFacetVisibility(facetId: string, shouldBeVisible: boolean) { + // your code +} + +/** + * Clean up a facet that we are no longer using. E.g.: when changing pages. + */ +function addOnFacetDestroyedListener(facetId: string) { + // your code +} +``` + +In the example above, the `makeDependent` function can be called to define a relationship between a dependent facet and one or more parent facets. + +You can modify the `condition` function to fit your particular use case. +For example, instead of verifying whether any value is selected (that is, equals `selected`) in the parent facet, you could verify the state of a specific value. + +We don’t provide sample implementations for the `setFacetVisibility` and `addOnFacetDestroyedListener` functions, as they will vary depending on which Web framework you’re using (for example, Angular, React, or Vue). + +## React Example + +To better understand this explanation, you can refer to this example made with React: +[Dependent Facet React Example](https://github.com/coveo/ui-kit/blob/master/samples/headless/search-react/src/pages/DependentFacetPage.tsx). \ No newline at end of file diff --git a/packages/headless/source_docs/extend-headless-controllers.md b/packages/headless/source_docs/extend-headless-controllers.md new file mode 100644 index 00000000000..5c328c2ab00 --- /dev/null +++ b/packages/headless/source_docs/extend-headless-controllers.md @@ -0,0 +1,52 @@ +--- +title: Extend controllers +group: Usage +slug: usage/extend-controllers +--- +# Extend controllers +When you call a method, Headless dispatches one or more low-level actions. +For example, calling the [`submit`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchBox.html#submit) method on the `SearchBox` controller dispatches actions to: + +* update the [query](https://docs.coveo.com/en/231/) +* clear active [facet](https://docs.coveo.com/en/198/) values +* reset the result page to the first page +* perform a query +* log the appropriate [Coveo Analytics event](https://docs.coveo.com/en/260/) + +Controllers embed user experience decisions that we believe are best-practices for creating great search experiences. +For example, submitting a query from a search box resets selected facet values to reduce the odds of seeing no results. +However, it’s possible to change the default behaviors by overriding or adding new methods that dispatch a different set of actions. +The following sample shows how to add a new method to the search box controller to perform a search request without resetting active facet values: + +```javascript +import { + buildSearchBox, + loadQueryActions, + loadPaginationActions, + loadSearchActions, + loadSearchAnalyticsActions, +} from '@coveo/headless' +  +function buildCustomSearchBox(engine: SearchEngine, id: string) { + const baseSearchBox = buildSearchBox(engine, {options: {id}}); + const {updateQuery} = loadQueryActions(engine); + const {updatePage} = loadPaginationActions(engine); + const {executeSearch} = loadSearchActions(engine); + const {logSearchboxSubmit} = loadSearchAnalyticsActions(engine); +  + return { + ...baseSearchBox, +  + submitWithoutFacetReset() { + const query = engine.state.querySet![id]; +  + updateQuery({q: query}); + updatePage(1); + executeSearch(logSearchboxSubmit()); + } + } +} +``` + +You can extend any Headless controller this way. +It’s possible to override existing methods, or add new ones that dispatch different sets of action \ No newline at end of file diff --git a/packages/headless/source_docs/headless-code-samples.md b/packages/headless/source_docs/headless-code-samples.md new file mode 100644 index 00000000000..26ace39c255 --- /dev/null +++ b/packages/headless/source_docs/headless-code-samples.md @@ -0,0 +1,22 @@ +--- +title: Code samples +slug: code-samples +--- +# Code samples + +The following interactive code sample uses Coveo Headless alongside the [Material-UI React framework](https://material-ui.com/) to create a simple search page. + + + +You can find controller-specific implementation examples in the controller [reference documentation](https://docs.coveo.com/en/headless/latest/reference/index.html). + +Finally, you can find more code samples in the following: + +* [Headless React](https://github.com/coveo/ui-kit/tree/master/samples/headless/search-react) +* [Headless Commerce React](https://github.com/coveo/ui-kit/tree/master/samples/headless/commerce-react) +* [Headless Next.js](https://github.com/olamothe/demo-next-js-headless) \ No newline at end of file diff --git a/packages/headless/source_docs/headless-modify-requests-responses.md b/packages/headless/source_docs/headless-modify-requests-responses.md new file mode 100644 index 00000000000..ba659418d4a --- /dev/null +++ b/packages/headless/source_docs/headless-modify-requests-responses.md @@ -0,0 +1,77 @@ +--- +title: Modify requests and responses +group: Usage +slug: usage/modify-requests-and-responses +--- +# Modify requests and responses +You may also want to modify responses before [Headless](https://docs.coveo.com/en/lcdf0493/) controllers use them. +This article explains how to do so. + +## Modify requests + +To modify requests sent to the Coveo Search or Usage Analytics APIs, use the `preprocessRequest` method of the target engine configuration (for example, [search](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchEngineConfiguration.html) or [recommendation](https://docs.coveo.com/en/headless/latest/reference/interfaces/Recommendation.RecommendationEngineConfiguration.html), depending on your use case). + +
❗ IMPORTANT
+ +The `preprocessRequest` method is a powerful tool, and it can be leveraged to do things that should be done in a different manner. +For example, you can use it to set [`aq`](https://docs.coveo.com/en/175/), but you should use the [Headless](https://docs.coveo.com/en/lcdf0493/) [`AdvancedSearchQuery`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.AdvancedSearchQueryActionCreators.html) action instead. + +If you have to use `preprocessRequest`, you should code defensively. +For example, you can implement [`try...catch`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/try%2E%2E%2Ecatch) to prevent errors. +
+ +```javascript +const engine = buildSearchEngine({ + configuration: { + // ... + preprocessRequest: (request, clientOrigin, metadata) => { ① + if (metadata?.method === 'search' && clientOrigin === 'searchApiFetch') { + const body = JSON.parse(request.body); + // E.g., modify facet requests + // body.facets = [...]; + request.body = JSON.stringify(body); + } + + if (clientOrigin === 'analyticsFetch') { + // E.g., send data to a third party + } + + return request; + }, + }, +}); +``` + +1. Initialize the function with its parameters: + * `request`: The HTTP request sent to Coveo. See [`preprocess-request.ts`](https://github.com/coveo/ui-kit/blob/master/packages/headless/src/api/preprocess-request.ts). + * `clientOrigin`: The origin of the request. See [`preprocess-request.ts`](https://github.com/coveo/ui-kit/blob/master/packages/headless/src/api/preprocess-request.ts). + * `metadata`: Optional metadata consisting of two properties: + + * `method`: The method called on the client. + * `origin`: The origin of the client that helps to distinguish between features while using the same method. + + See [`search-metadata.ts`](https://github.com/coveo/ui-kit/blob/master/packages/headless/src/api/search/search-metadata.ts). + +## Modify responses + +If you’re using the search engine, you can leverage the [search configuration options](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchConfigurationOptions.html) to modify Search API responses before [Headless](https://docs.coveo.com/en/lcdf0493/) controllers use them. +Use the `preprocessSearchResponseMiddleware`, `preprocessFacetSearchMiddleware`, or `preprocessQuerySuggestResponseMiddleware` method, depending on the target endpoint. + +```javascript +const engine = buildSearchEngine({ + configuration: { + // ... + search: { + preprocessSearchResponseMiddleware: (response) => { + response.body.results.forEach((result) => { + // E.g., modify the result object + return result; + }); + return response; + }, + preprocessFacetSearchResponseMiddleware: (response) => response, + preprocessQuerySuggestResponseMiddleware: (response) => response, + }, + }, +}); +``` \ No newline at end of file diff --git a/packages/headless/source_docs/headless-proxy.md b/packages/headless/source_docs/headless-proxy.md new file mode 100644 index 00000000000..e13f8bec3cd --- /dev/null +++ b/packages/headless/source_docs/headless-proxy.md @@ -0,0 +1,172 @@ +--- +title: Proxy +group: Usage +slug: usage/proxy +--- +# Headless proxy + +Coveo Headless engines and certain actions expose `proxyBaseUrl` options. +You should only set this advanced option if you need to proxy Coveo API requests through your own server. +In most cases, you shouldn’t set this option. + +By default, no proxy is used and the Coveo API requests are sent directly to the Coveo platform through [organization endpoints](https://docs.coveo.com/en/mcc80216/) resolved from the `ORGANIZATION_ID` and `ENVIRONMENT` values provided in your engine configuration, such as `.org.coveo.com` or `.org.coveo.com`, if the `ENVIRONMENT` values is specified and different from `prod`. + +If you set this option, you must also implement the required proxy endpoints in your server, otherwise the Headless engine won’t work properly. +The exact endpoints you need to implement depend on the `proxyBaseUrl` you want to set. + +## Analytics + +**Example** + +```ts +import { buildSearchEngine } from '@coveo/headless'; + +const searchEngine = buildSearchEngine({ + organizationId: 'my-org-id', + accessToken: 'my-access-token', + analytics: { + proxyBaseUrl: 'https://search-proxy.example.com', + }, +}); +``` + +If you set the `proxyBaseUrl` option in your engine analytics configuration, you must also implement the correct proxy endpoints in your server, depending on the `analyticsMode` you’re using. + +If you’re using the `next` analytics mode, you must implement the following proxy endpoints: + +* `POST` `/` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/organizations/{ORGANIZATION_ID}/events/v1`](https://platform.cloud.coveo.com/docs?urls.primaryName=Event#/Event%20API/rest_organizations_paramId_events_v1_post) +* `POST` `/validate` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/organizations/{ORGANIZATION_ID}/events/v1/validate`](https://platform.cloud.coveo.com/docs?urls.primaryName=Event#/Event%20API/rest_organizations_paramId_events_v1_validate_post) + +The [Event Protocol Reference](https://docs.coveo.com/en/n9da0377) provides documentation on the analytics event schemas that can be passed as request bodies to the above endpoints. + +If you’re using the `legacy` analytics mode, your `proxyBaseUrl` must end with `/rest/v15/analytics`, and you must implement the following proxy endpoints: + +* `POST` `/click` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/v15/analytics/click`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/post__v15_analytics_click) +* `POST` `/collect` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/v15/analytics/collect`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/post__v15_analytics_collect) +* `POST` `/custom` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/v15/analytics/custom`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/post__v15_analytics_custom) +* `GET` `/monitoring/health` to proxy requests to `GET` [`.analytics.org.coveo.com/rest/v15/analytics/monitoring/health`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/get__v15_analytics_monitoring_health) +* `POST` `/search` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/v15/analytics/search`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/post__v15_analytics_search) +* `POST` `/searches` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/v15/analytics/searches`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/post__v15_analytics_searches) +* `GET` `/status` to proxy requests to `GET` [`.analytics.org.coveo.com/rest/v15/analytics/status`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/get__v15_analytics_status) +* `POST` `/view` to proxy requests to `POST` [`.analytics.org.coveo.com/rest/v15/analytics/view`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/post__v15_analytics_view) +* `DELETE` `/visit` to proxy requests to `DELETE` [`.analytics.org.coveo.com/rest/v15/analytics/visit`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/delete__v15_analytics_visit) +* `GET` `/visit` to proxy requests to `GET` [`.analytics.org.coveo.com/rest/v15/analytics/visit`](https://docs.coveo.com/en/18#tag/Analytics-API-Version-15/operation/get__v15_analytics_visit) + +## Search + +**Example** + +```ts +import { buildSearchEngine } from '@coveo/headless'; + +const searchEngine = buildSearchEngine({ + organizationId: 'my-org-id', + accessToken: 'my-access-token', + search: { + proxyBaseUrl: 'https://search-proxy.example.com', + }, +}); +``` + +If you’re setting the `proxyBaseUrl` option in your search engine or action configuration, you must also implement the correct proxy endpoints in your server, depending on the search API requests you want to proxy. + +* `POST` `/` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2`](https://docs.coveo.com/en/13#tag/Search-V2/operation/searchUsingPost) +* `POST` `/plan` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/plan`](https://docs.coveo.com/en/13#tag/Search-V2/operation/planSearchUsingPost) +* `POST` `/querySuggest` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/querySuggest`](https://docs.coveo.com/en/13#tag/Search-V2/operation/querySuggestPost) +* `POST` `/facet` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/facet`](https://docs.coveo.com/en/13#tag/Search-V2/operation/facetSearch) +* `POST` `/html` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/html`](https://docs.coveo.com/en/13#tag/Search-V2/operation/htmlPost) +* `GET` `/fields` to proxy requests to `GET` [`.org.coveo.com/rest/search/v2/fields`](https://docs.coveo.com/en/13#tag/Search-V2/operation/fields) + +## Case Assist + +**Example** + +```ts +import { buildCaseAssistEngine } from '@coveo/headless/case-assist'; + +const caseAssistEngine = buildCaseAssistEngine({ + caseAssistId: 'my-case-assist-id', + organizationId: 'my-organization-id', + accessToken: 'my-access-token', + proxyBaseUrl: 'https://case-assist-proxy.example.com', +}); +``` + +If you’re setting the `proxyBaseUrl` option in your case assist engine, action or state configuration, you must also implement the following proxy endpoints in your server, otherwise the case assist engine won’t work properly: + +* `POST` `/classify` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//caseassists//classify`](https://docs.coveo.com/en/3430#tag/Case-Assist/operation/postClassify) +* `POST` `/documents/suggest` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//caseassists//documents/suggest`](https://docs.coveo.com/en/3430#tag/Case-Assist/operation/getSuggestDocument) + +## Commerce + +**Example** + +```ts +import { buildCommerceEngine } from '@coveo/headless/commerce'; + +const commerceEngine = buildCommerceEngine({ + configuration: { + organizationId: 'my-org-id', + accessToken: 'my-access-token', + proxyBaseUrl: 'https://commerce-proxy.example.com', + }, +}); +``` + +If you’re setting the `proxyBaseUrl` option in your commerce engine or action configuration, you must also implement the following proxy endpoints in your server, otherwise the commerce engine won’t work properly: + +* `POST` `/facet` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//commerce/v2/facet`](https://docs.coveo.com/en/103#tag/Facet/operation/facet) +* `POST` `/listing` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//commerce/v2/listing`](https://docs.coveo.com/en/103#tag/Listings/operation/getListing) +* `POST` `/productSuggest` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//commerce/v2/search/productSuggest`](https://docs.coveo.com/en/103#tag/Search/operation/productSuggest) +* `POST` `/querySuggest` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//commerce/v2/search/querySuggest`](https://docs.coveo.com/en/103#tag/Search/operation/querySuggest) +* `POST` `/recommendations` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//commerce/v2/recommendations`](https://docs.coveo.com/en/103#tag/Recommendations/operation/recommendations) +* `POST` `/search` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//commerce/v2/search`](https://docs.coveo.com/en/103#tag/Search/operation/search) + +## Insight engine + +**Example** + +```ts +import { buildInsightEngine } from '@coveo/headless/insight'; + +const searchEngine = buildInsightEngine({ + organizationId: 'my-org-id', + accessToken: 'my-access-token', + search: { + proxyBaseUrl: 'https://insight-proxy.example.com', + }, +}); +``` + +If you’re setting the `proxyBaseUrl` option in your insight engine configuration, you must also implement the following proxy endpoints in your server, otherwise the insight engine won’t work properly: + +* `GET` `/interface` to proxy requests to `GET` [`.org.coveo.com/rest/organizations//insight/v1/configs//interface`](https://docs.coveo.com/en/3430#tag/Insight-Panel/operation/insight-panel-interface-get) +* `POST` `/querySuggest` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//insight/v1/configs//querySuggest`](https://docs.coveo.com/en/3430#tag/Insight-Panel/operation/insight-panel-query-suggest) +* `GET` `/quickview` to proxy requests to `GET` [`.org.coveo.com/rest/organizations//insight/v1/configs//quickview`](https://docs.coveo.com/en/3430#tag/Insight-Panel/operation/insight-panel-quickview) +* `POST` `/search` to proxy requests to `POST` [`.org.coveo.com/rest/organizations//insight/v1/configs//search`](https://docs.coveo.com/en/3430#tag/Insight-Panel/operation/insight-panel-search) + +## Recommendation engine + +**Example** + +```ts +import { buildRecommendationEngine } from '@coveo/headless/recommendation'; + +const RecommendationEngine = buildRecommendationEngine({ + configuration: { + caseAssistId: 'my-case-assist-id', + organizationId: 'my-organization-id', + accessToken: 'my-access-token', + proxyBaseUrl: 'https://recommendation-proxy.example.com', + }, +}); +``` + +If you’re setting the `proxyBaseUrl` option in your recommendation engine configuration, you must also implement the following proxy endpoints in your server, otherwise the recommendation engine won’t work properly: + +* `POST` `/` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2`](https://docs.coveo.com/en/13#tag/Search-V2/operation/searchUsingPost) +* `POST` `/plan` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/plan`](https://docs.coveo.com/en/13#tag/Search-V2/operation/planSearchUsingPost) +* `POST` `/querySuggest` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/querySuggest`](https://docs.coveo.com/en/13#tag/Search-V2/operation/querySuggestPost) +* `POST` `/facet` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/facet`](https://docs.coveo.com/en/13#tag/Search-V2/operation/facetSearch) +* `POST` `/html` to proxy requests to `POST` [`.org.coveo.com/rest/search/v2/html`](https://docs.coveo.com/en/13#tag/Search-V2/operation/htmlPost) +* `GET` `/fields` to proxy requests to `GET` [`.org.coveo.com/rest/search/v2/fields`](https://docs.coveo.com/en/13#tag/Search-V2/operation/fields) diff --git a/packages/headless/source_docs/headless-saml-authentication.md b/packages/headless/source_docs/headless-saml-authentication.md new file mode 100644 index 00000000000..c602a3c211a --- /dev/null +++ b/packages/headless/source_docs/headless-saml-authentication.md @@ -0,0 +1,91 @@ +--- +title: SAML authentication +group: Usage +slug : usage/saml-authentication +--- +# Configure SAML authentication +The [Search API](https://docs.coveo.com/en/52/) supports SAML authentication, and the [`@coveo/auth`](https://github.com/coveo/ui-kit/tree/master/packages/auth) package lets you easily enable SAML authentication in a search page built with the [Coveo Headless library](https://docs.coveo.com/en/lcdf0493/). + +## Install `@coveo/auth` + +Before you can configure SAML with Headless, you must install the `@coveo/auth` package as a dependency. +You can do this with the following npm command: + +```bash +npm i @coveo/auth +``` + +## Configure SAML + +First, [Configure a SAML identity provider using the Search API](https://docs.coveo.com/en/91/). +You’ll need it when creating your Headless interface. + +## Headless code sample + +The code example below is a sample [SAML page built with Headless](https://github.com/coveo/ui-kit/blob/master/samples/headless/search-react/src/pages/SamlPage.tsx) and [React](https://reactjs.org/). + +```tsx +import {buildSamlClient, SamlClient, SamlClientOptions} from '@coveo/auth'; +import {buildSearchEngine} from '@coveo/headless'; +import { + useState, + useMemo, + useRef, + useEffect, + PropsWithChildren, + FunctionComponent, +} from 'react'; +import {AppContext} from '../context/engine'; + +const samlClientOptions: SamlClientOptions = { <1> + organizationId: '', + provider: '', +}; + +export const SamlPage: FunctionComponent = ({children}) => { + const [initialAccessToken, setInitialAccessToken] = useState(''); <2> + const samlClient = useRef(null); <3> + useEffect(() => { <4> + if (samlClient.current) { + return; <5> + } + samlClient.current = buildSamlClient(samlClientOptions); <6> + samlClient.current.authenticate().then(setInitialAccessToken); + }, []); + + const engine = useMemo( <7> + () => + initialAccessToken && samlClient.current + ? buildSearchEngine({ + configuration: { + organizationId: samlClientOptions.organizationId, + accessToken: initialAccessToken, + renewAccessToken: samlClient.current.authenticate, + }, + }) + : null, + [samlClientOptions, samlClient.current, initialAccessToken] + ); + + if (!engine) { + return null; + } + + return {children}; <8> +}; +``` + +1. `SamlClientOptions` is a `@coveo/auth` interface that lets you define the parameters for the organization and the SAML provider. +The organization is identified with the `organizationId` (for example, `myorg1n23b18d5a`), while the `provider` (for example, `mySAMLAuthenticationProvider`) parameter refers to the SAML identity provider that you configured with the Search API. +2. `initialAccessToken` is defined as a [state](https://beta.reactjs.org/learn/state-a-components-memory) of the `SamlPage` component. +It’s initialized as an empty string using the `useState` hook. +`setInitialAccessToken`, its corresponding state setter function, is also defined. +3. `SamlClient` is a `@coveo/auth` interface that is responsible for initiating the SAML flow to resolve a Coveo access token. +The [`useRef`](https://beta.reactjs.org/reference/react/useRef) hook is used here, because the value of `samlClient` isn’t needed for rendering. +4. The [`useEffect`](https://beta.reactjs.org/reference/react/useEffect) hook is used to initialize `samlClient.current`. +5. `SamlClient.authenticate` is not idempotent. +Calling it twice after redirection from the provider, even on different clients, would cause a redirection loop. +6. `buildSamlClient` is a function used to instantiate `SamlClient`. +7. The [`useMemo`](https://beta.reactjs.org/reference/react/useMemo) hook is used to cache values between re-renders. +In this case, `organizationId`, `accessToken`, and `renewAccessToken` are used to configure the [`buildSearchEngine`](https://docs.coveo.com/en/headless/latest/usage#configure-a-headless-engine) function. +8. The [`Context.Provider`](https://reactjs.org/docs/context.html#contextprovider) React component is returned. \ No newline at end of file diff --git a/packages/headless/source_docs/headless-usage-analytics-coveo-ua.md b/packages/headless/source_docs/headless-usage-analytics-coveo-ua.md new file mode 100644 index 00000000000..d853dfc74aa --- /dev/null +++ b/packages/headless/source_docs/headless-usage-analytics-coveo-ua.md @@ -0,0 +1,334 @@ +--- +title: Coveo UA +group: Usage +category: Usage Analytics +slug: usage/usage-analytics/coveo-ua +--- +# Coveo UA + +When used correctly, Headless controllers take care of logging standard Coveo UA search and click [Coveo Analytics events](https://docs.coveo.com/en/260/) for you. +This article covers various topics that you may find helpful if you require further customization when using the Coveo UA protocol with Headless. + +
📌 Notes
+ +* For brevity, this article mainly focuses on the [`Search Engine`](https://docs.coveo.com/en/headless/latest/reference/modules/Search.html). +However, similar logic applies when configuring UA for other Headless engines (except the Commerce Engine, which only supports [Event Protocol](https://docs.coveo.com/en/o3r90189/)). +* Take a look at the [Log view events with Coveo UA](https://docs.coveo.com/en/headless/latest/usage/headless-usage-analytics/headless-view-events/) article to understand how to log view events. +View event tracking with the Coveo UA protocol requires the `coveoua.js` script rather than the [Atomic](https://docs.coveo.com/en/lcdf0264/) or Headless libraries. +
+ +## Modify the metadata to send with UA events + +It can be useful to add or modify metadata to send along with the standard UA events logged by Headless controllers. +You can leverage the `analyticsClientMiddleware` property of an [`AnalyticsConfiguration`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.AnalyticsConfiguration.html) to hook into an analytics event payload before Headless sends it to Coveo. +By default, Headless v3 uses [Event Protocol](https://docs.coveo.com/en/o9je0592/) to track events instead of the Coveo UA protocol. +The Event Protocol doesn’t support the `analyticsClientMiddleware` property. +To use this property in Headless v3, you need to [set the `analyticsMode` to `legacy`](https://docs.coveo.com/en/headless/latest/headless-upgrade-from-v2/#removal-of-analyticsclientmiddleware-function) during the initialization. + +The following example shows how to customize metadata using `analyticsClientMiddleware`: + +```jsx +const analyticsClientMiddleware = (eventName, payload) => { ② + if (payload.visitorId == "") { ③ + payload.customData['loggedIn'] = false // new metadata field added + payload.customData['context_role'] = "Anonymous" + } else { + payload.customData['loggedIn'] = true + payload.customData['context_role'] = "Visitor" + } + return payload; +}; + +export const headlessEngine = buildSearchEngine({ + configuration: { + organizationId: "", + accessToken: "", + analytics: { + analyticsClientMiddleware ① + }, + } +}) +``` + +1. The `analyticsClientMiddleware` is a function that needs to be defined if we want to add or modify event data (see [analytics.ts](https://github.com/coveo/coveo.analytics.js/blob/master/src/client/analytics.ts)). +2. The function takes as input an `eventName` and the event `payload`. +3. Within this function, you can access the `payload` data and modify it. +In the example above, we check to see if `visitorId` is an empty string. +We add a new field (`loggedIn`) and a new custom context field (`context_role`) to `customData`. +If `visitorId` is empty, `loggedIn` is set to `false` and `context_role` to `Anonymous`. +On the other hand, if `visitorId` is not empty, `loggedIn` is set to `true` and `context_role` to `Visitor`. + +## Send click events + +Click events are intended to record item view and preview actions, such as: + +* Opening a result link +* Opening a result Quick view + +
⚠️ WARNING
+ +We strongly recommend using the [`InteractiveResult`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.InteractiveResult.html) controller when implementing your result components. +The controller can automatically extract relevant data from result items and log click events for you, as in the following interactive example. +
+ + + +To learn more about using the `InteractiveResult` component in your result list implementation, see [Lesson 3](https://levelup.coveo.com/learn/courses/headless-commerce-tutorial/lessons/lesson-3-usage-analytics#_click_events) of the Coveo Headless Tutorial. + +### Send your own click events + +It’s also technically possible to send your own click events, without the `InteractiveResult` controller, by [dispatching](https://docs.coveo.com/en/headless/latest/usage#dispatch-actions) [`ClickAnalyticsActions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ClickAnalyticsActionCreators.html) or [`GenericAnalyticsActions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.GenericAnalyticsActionCreators.html). + +However, we recommend against doing so because it’s very error prone. +For UA reports and ML models to function properly, click events need to contain all the metadata that the `InteractiveResult` controller extracts from results. + +If you need to customize your click events, we rather recommend [using the `analyticsClientMiddleware` property](#modify-the-metadata-to-send-with-ua-events) and listening to the [target action](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ClickAnalyticsActionCreators.html), as in the following example. + +```jsx +export const headlessEngine = buildSearchEngine({ + configuration: { + organizationId: "", + accessToken: "", + analytics: { + analyticsClientMiddleware: (eventName: string, payload: any) => { + if (payload.actionCause === 'documentOpen') { ① + const matchingResult = headlessEngine.state.search.results[payload.documentPosition - 1]; + payload.customData['intent'] = matchingResult.raw['docsintent']; ② + } + return payload; + }; + }, + } +}) +``` + +1. If a UA event is dispatched with the [`logDocumentOpen`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ClickAnalyticsActionCreators.html#logDocumentOpen) cause, add the target metadata. +2. You can access result item fields when logging metadata. +Concretely, you can use the populated default item fields, plus the ones specified through the [`fieldsToInclude`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ResultListOptions.html) parameter. +You can inspect search request responses in your search interface to see the currently available fields: + +![Inspecting response fields](https://docs.coveo.com/en/assets/images/build-a-search-ui/inspect-fields.png) + +## Send search events + +Search events are intended to record end-user interactions that trigger queries, such as: + +* Submitting a search request from the search box +* Selecting a facet value + +You generally shouldn’t have to worry about logging search events, because standard search controllers such as [`SearchBox`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchBox.html) and [`facet`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.Facet.html) controllers take care of automatically logging such interactions. + +### Send your own search events + +If you need to send additional search events, you can do so by [dispatching actions](https://docs.coveo.com/en/headless/latest/usage#dispatch-actions). +We recommend using controllers over dispatching actions directly. +However, the latter is still possible and can be helpful in specific use cases that controllers don’t cover, such as when you need to send your own UA events. + +Depending on your use case, there are two ways to send your own search events. +A search event can either be sent via [`SearchAnalyticsActions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchAnalyticsActionCreators.html) or [`GenericAnalyticsActions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.GenericAnalyticsActionCreators.html). +Both of these are action loaders. + +`SearchAnalyticsActions` are for specific search events for which the _cause_ is recognized by Coveo, such as `logFacetClearAll` or `logInterfaceLoad`. +These events are used by Coveo ML. + +`GenericAnalyticsActions` lets you send any type of custom search event using the `logSearchEvent` action creator. +The _cause_ of this event is unknown to Coveo, so it can’t be used by Coveo ML. + +If you want to use `SearchAnalyticsActions`, we recommend implementing logic such as in the following example. + +```jsx +import {headlessEngine} from '../engine'; ① +import {loadSearchActions} from '@coveo/headless'; ② +import {loadSearchAnalyticsActions} from '@coveo/headless'; ③ + +const searchAction = () => { + const {executeSearch} = loadSearchActions(headlessEngine); ④ + const {logInterfaceLoad} = loadSearchAnalyticsActions(headlessEngine); ⑤ + headlessEngine.dispatch(executeSearch(logInterfaceLoad())); ⑥ +}; + +export const CustomComponent = () => { + return ( + + ) +}; +``` + +1. Import a local initialized search engine from your `engine.ts` file. +2. Import `loadSearchActions` from the Headless package. +This lets you create an action to execute a search query. +3. Import `loadSearchAnalyticsActions` from the Headless package. +This will lets you return a dictionary of possible search analytics action creators. +4. Get the `executeSearch` action creator to execute a search query. +5. Get a specific action creator, `logInterfaceLoad` in this scenario. +This is the analytics event you will log. +6. Dispatch an action to execute a search query, with the search analytic action passed in as input to log it. + +
📌 Note
+ +Take a look at [Custom events](#send-custom-events) to see a code example of how to log a search event using `GenericAnalyticsActions`. +
+ +## Send custom events + +Custom events are intended to record end-user interactions that don’t trigger a query or open a query result, such as: + +* Updating end-user preferences +* Changing the result list layout + +`GenericAnalyticsActions` lets you send any type of custom event using the `logCustomEvent` action creator. +The _cause_ of this event is unknown to Coveo, so it can’t be used by Coveo ML. + +If you want to use `GenericAnalyticsActions`, we recommend implementing logic such as in the following example. + +```jsx +import {headlessEngine} from '../engine'; ① +import {loadGenericAnalyticsActions} from '@coveo/headless'; ② + +const genericCustomAction = () => { + const {logCustomEvent} = loadGenericAnalyticsActions(headlessEngine) ③ + const payload = { ④ + evt: '', + type: '' + } + headlessEngine.dispatch(logCustomEvent(payload)) ⑤ +} + +export const CustomComponent = () => { + return ( + + ) +}; +``` + +1. Import a local initialized search engine from your `engine.ts` file. +2. Import `loadGenericAnalyticsActions` from the Headless package. +This lets you return a dictionary of possible generic analytics action creators. +3. Get a specific action creator, `logCustomEvent` in this scenario. +4. Create a `payload` object to be sent to Coveo when logging a custom event. +This payload will describe the details of which event led to the action being triggered. +5. Dispatch the action to log the custom event. + +## User tracking and anonymizing UA Data + +By default, the Usage Analytics Write API will extract the `name` and `userDisplayName`, if present, from the [search token](https://docs.coveo.com/en/56/). +If the users of your search interface are authenticated, you may want to hash their identities to ensure that they can’t be clearly identified in UA data. +You can do so when initializing an engine instance by setting the `anonymous` property of the `AnalyticsConfiguration` as in the example below. + +```jsx +export const headlessEngine = buildSearchEngine({ + configuration: { + organizationId: "", + accessToken: "", + analytics: { + anonymous: true + }, + } +}) +``` + +While we recommend the use of a search token for request authentication, it’s still possible to send user information if users are logged in, and you’re utilizing an [API key](https://docs.coveo.com/en/105/) for authentication. + +When using an API key, user information can be sent to Coveo by [modifying the UA event](#modify-the-metadata-to-send-with-ua-events), as shown in the following code snippet: + +
❗ IMPORTANT
+ +When you [create the API Key](https://docs.coveo.com/en/1718#create-an-api-key), use the **Anonymous search** [template](https://docs.coveo.com/en/1718#api-key-templates). +It will provide the right [privileges](https://docs.coveo.com/en/228/) for this use case. +
+ +```typescript +export const headlessEngine = buildSearchEngine({ + configuration: { + organizationId: "", + accessToken: "", + analytics: { + analyticsClientMiddleware: (eventName: string, payload: any) => { + if isLoggedIn { ① + payload.username = ; + payload.userDisplayName = ; + } + return payload; + }; + }, + } +}); +``` +1. Use a custom `isLoggedIn` variable to determine whether the user is logged in. +Extract the `username` and `userDisplayName` from your user object and add them to the payload. + +## Send events externally + +If you want to log UA events to an external service, such as [Google Analytics](https://analytics.google.com/), we recommend leveraging the `analyticsClientMiddleware` property in your [`AnalyticsConfiguration`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.AnalyticsConfiguration.html) to hook into an analytics event payload, as in the following example. + +```jsx +const pushToGoogleDataLayer = (payload: Record) => { + // For implementation details, see the Google documentation + // (https://developers.google.com/tag-platform/tag-manager/web/datalayer#datalayer) +}; +// ... +(async () => { + await customElements.whenDefined('atomic-search-interface'); + const searchInterface = document.querySelector('atomic-search-interface'); + await searchInterface.initialize({ + accessToken: '', + organizationId: '', + analytics: { + analyticsClientMiddleware: (eventType, payload) => { + pushToGoogleDataLayer(payload); + return payload; + }, + } + }), +})(); +``` + +## Disable and enable analytics + +Coveo front-end libraries use the `coveo_visitorId` cookie to track individual users and sessions. + +
📌 Note
+ +Coveo now uses the [client ID](https://docs.coveo.com/en/lbjf0131/) value to track individual users and sessions. +For compatibility with legacy implementations, however, the associated cookie and local storage value are [still labeled `visitorID`](https://docs.coveo.com/en/mc2e2218#why-do-i-still-see-the-name-visitor-id-in-the-local-storage). +
+ +When implementing a cookie policy, you may need to disable UA tracking for end-users under specific circumstances (for example, when a user opts out of cookies). +To do so, call the [`disableAnalytics`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchEngine.html#disableAnalytics) method on an engine instance. + +To re-enable UA tracking, call the [`enableAnalytics`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchEngine.html#enableAnalytics) method. + +```typescript +// initialize an engine instance +const headlessEngine = buildSearchEngine({ + configuration: getSampleSearchEngineConfiguration(), +}); + +headlessEngine.disableAnalytics() +// Or, headlessEngine.enableAnalytics(); +``` + +## doNotTrack property + +[`doNotTrack`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is a browser property which reflects the value of the [`DNT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT) HTTP header. +It’s used to indicate whether the user is requesting sites and advertisers not to track them. + +
📌 Note
+ +This property is deprecated, but it’s still supported in many browsers. +
+ +Headless v2 complies with the value of this property. +It automatically disables analytics tracking whenever `DNT` is enabled. + +
❗ IMPORTANT
+ +Headless v3 will no longer support this property. +
+ +
📌 NOTE
+ +To understand how Coveo Usage Analytics tracks users and sessions, see [What’s a user visit?](https://docs.coveo.com/en/1873/). +
\ No newline at end of file diff --git a/packages/headless/source_docs/headless-usage-analytics-ep.md b/packages/headless/source_docs/headless-usage-analytics-ep.md new file mode 100644 index 00000000000..20f3717eba8 --- /dev/null +++ b/packages/headless/source_docs/headless-usage-analytics-ep.md @@ -0,0 +1,106 @@ +--- +title: Event Protocol +group: Usage +category: Usage Analytics +slug: usage/usage-analytics/event-protocol +--- +# Event Protocol with Headless + +
❗ IMPORTANT
+ +[Event Protocol](https://docs.coveo.com/en/o9je0592/) is currently in open beta. +If you’re interested in using Event Protocol, reach out to your Customer Success Manager (CSM). +
+ +Since the Headless V3 release, [Event Protocol](https://docs.coveo.com/en/o9je0592/) is the default tracking protocol. + +
❗ IMPORTANT
+ +Only [Coveo for Commerce](https://docs.coveo.com/en/1499/) supports EP at the moment. +For every other kind of implementation, set `analyticsMode` to `legacy` for the time being. +
+ +```ts +const engine = buildSearchEngine({ + configuration: { + // ...rest of configuration + analytics: {analyticsMode: 'legacy'}, + } +}) +``` + +To learn more about whether you should migrate to EP, see [Migrate to Event Protocol](https://docs.coveo.com/en/o88d0509/). + +## Log events + +EP is simpler to use and more streamlined than the legacy Coveo UA protocol. +EP doesn’t support custom events, custom data, or custom context. +Search events are sent server-side, so you don’t need to log them client-side. +Headless controllers, when used correctly, also log click events for you. + +As a result, you generally won’t need to tinker with search or click events yourself. + +
⚠️ WARNING
+ +We strongly recommend using the [`InteractiveResult`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.InteractiveResult.html) controller when implementing your result components. +The controller can automatically extract relevant data from result items and log click events for you, as in the following interactive example. +
+ + + +
📌 Note
+ +For the purpose of using [content recommendations](https://docs.coveo.com/en/1016/) models however, you must log [view events](https://docs.coveo.com/en/headless/latest/usage/headless-usage-analytics/headless-view-events-ep/). +Headless controllers won’t log view events for you. +Use the [Relay library](https://docs.coveo.com/en/headless/latest/usage/headless-usage-analytics/headless-view-events-ep/). +
+ +## Disable and enable analytics + +Coveo front-end libraries use the `coveo_visitorId` cookie to track individual users and sessions. + +
📌 Note
+ +Coveo now uses the [client ID](https://docs.coveo.com/en/lbjf0131/) value to track individual users and sessions. +For compatibility with legacy implementations, however, the associated cookie and local storage value are [still labeled `visitorID`](https://docs.coveo.com/en/mc2e2218#why-do-i-still-see-the-name-visitor-id-in-the-local-storage). +
+ +When implementing a cookie policy, you may need to disable UA tracking for end-users under specific circumstances (for example, when a user opts out of cookies). +To do so, call the [`disableAnalytics`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchEngine.html#disableAnalytics) method on an engine instance. + +To re-enable UA tracking, call the [`enableAnalytics`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchEngine.html#enableAnalytics) method. + +```typescript +// initialize an engine instance +const headlessEngine = buildSearchEngine({ + configuration: getSampleSearchEngineConfiguration(), +}); + +headlessEngine.disableAnalytics() +// Or, headlessEngine.enableAnalytics(); +``` + +## doNotTrack property + +[`doNotTrack`](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/doNotTrack) is a browser property which reflects the value of the [`DNT`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/DNT) HTTP header. +It’s used to indicate whether the user is requesting sites and advertisers not to track them. + +
📌 Note
+ +This property is deprecated, but it’s still supported in many browsers. +
+ +Headless v2 complies with the value of this property. +It automatically disables analytics tracking whenever `DNT` is enabled. + +
❗ IMPORTANT
+ +Headless v3 will no longer support this property. +
+ +
📌 NOTE
+ +To understand how Coveo Usage Analytics tracks users and sessions, see [What’s a user visit?](https://docs.coveo.com/en/1873/). +
\ No newline at end of file diff --git a/packages/headless/source_docs/headless-usage-analytics.md b/packages/headless/source_docs/headless-usage-analytics.md new file mode 100644 index 00000000000..9cfb5ab0580 --- /dev/null +++ b/packages/headless/source_docs/headless-usage-analytics.md @@ -0,0 +1,47 @@ +--- +title: Introduction +group: Usage +category: Usage Analytics +slug: usage/usage-analytics/index +--- +# Usage Analytics + +Administrators can leverage recorded [Coveo Analytics](https://docs.coveo.com/en/182/) [data](https://docs.coveo.com/en/259/) to evaluate how users interact with a [search interface](https://docs.coveo.com/en/2741/), identify content gaps, and improve relevancy by generating and examining [dashboards](https://docs.coveo.com/en/256/) and [reports](https://docs.coveo.com/en/266/) within the [Coveo Administration Console](https://docs.coveo.com/en/183/) (see [Coveo Analytics overview](https://docs.coveo.com/en/l3bf0598/)). +Moreover, [Coveo Machine Learning (Coveo ML)](https://docs.coveo.com/en/188/) features require UA data to function. + +Since version 2.80.0, Coveo Headless supports two tracking protocols: the [Coveo UA Protocol](https://docs.coveo.com/en/headless/latest/usage/headless-usage-analytics/headless-coveo-ua/) and the new [Event Protocol (EP)](https://docs.coveo.com/en/headless/latest/usage/headless-usage-analytics/headless-ep/). + +Event Protocol is a new standard for sending analytics [events](https://docs.coveo.com/en/260/) to Coveo. + +It simplifies event tracking by reducing the number of events sent to Coveo. +For instance, you no longer need to send any `search` events as they’re logged server-side when you make a request to the Search API. + +The protocol also streamlines the event payload, requiring fewer fields. +Such streamlining results in a more efficient and less error-prone event tracking process, yielding better [machine learning](https://docs.coveo.com/en/188/) [models](https://docs.coveo.com/en/1012/) and [Coveo Analytics](https://docs.coveo.com/en/182/) [reports](https://docs.coveo.com/en/266/). + +## Coveo UA vs. EP + +
❗ IMPORTANT
+ +[Event Protocol](https://docs.coveo.com/en/o9je0592/) is currently in open beta. +If you’re interested in using Event Protocol, reach out to your Customer Success Manager (CSM). +
+ +Since the Headless V3 release, [Event Protocol](https://docs.coveo.com/en/o9je0592/) is the default tracking protocol. + +
❗ IMPORTANT
+ +Only [Coveo for Commerce](https://docs.coveo.com/en/1499/) supports EP at the moment. +For every other kind of implementation, set `analyticsMode` to `legacy` for the time being. +
+ +```ts +const engine = buildSearchEngine({ + configuration: { + // ...rest of configuration + analytics: {analyticsMode: 'legacy'}, + } +}) +``` + +To learn more about whether you should migrate to EP, see [Migrate to Event Protocol](https://docs.coveo.com/en/o88d0509/). \ No newline at end of file diff --git a/packages/headless/source_docs/headless-view-events-ep.md b/packages/headless/source_docs/headless-view-events-ep.md new file mode 100644 index 00000000000..271ee08dbb4 --- /dev/null +++ b/packages/headless/source_docs/headless-view-events-ep.md @@ -0,0 +1,57 @@ +--- +title: Log view events with EP +group: Usage +category: Usage Analytics +slug: usage/usage-analytics/log-view-events-with-ep +--- +# Log view events with EP + +You can use Headless controllers to handle Search API requests and leverage [Coveo Analytics](https://docs.coveo.com/en/182/). +Headless-powered search UI components can automatically click [events](https://docs.coveo.com/en/260/) for user interactions in your [search interfaces](https://docs.coveo.com/en/2741/). +These events let you track user interactions with your [search interfaces](https://docs.coveo.com/en/2741/) so that you can optimize your Coveo solution. +Search and click [events](https://docs.coveo.com/en/260/) provide the data to power most [Coveo Machine Learning (Coveo ML)](https://docs.coveo.com/en/188/) [models](https://docs.coveo.com/en/1012/), except for [Content Recommendations (CR)](https://docs.coveo.com/en/1016/). + +The output of a CR model depends on [view](https://docs.coveo.com/en/2949#view) events and the user’s action history. +Headless doesn’t log these events for you, so you should ensure that you’re [sending view events](#send-view-events-with-relay) for each page that you want to be able to recommend. + +The [Relay library](https://docs.coveo.com/en/relay/latest/), which is included in Headless, lets you send EP view events to Coveo. + +
💡 TIP: Leading practice
+ +Start sending view events as soon as you can to gather data that your CR models can learn from. +
+ +## Send view events with Relay + +Use the `relay.emit` function on your Engine to send events to Coveo. +The `emit` function accepts two parameters: the event type and the event payload. + +The event type is the `string` name of the event, in this case `itemView`, and the event payload is a JSON object containing the data to be sent to Coveo. + +```js +export const searchEngine = buildSearchEngine({ + configuration: { + // ... + analytics: { + trackingId: "sports", + analyticsMode: "next", + }, + }, +}); + +searchEngine.relay.emit('itemView', { ① + itemMetadata: { ② + uniqueFieldName: "permanentid", + uniqueFieldValue: "kayak-paddle-01", + author: "Barca Sports", + url: "https://www.mydomain.com/l/products/kayak-paddle-01", + title: "Bamboo Kayak Paddle" + }, +}); +``` +1. Pass in the name of the event as the first parameter of the `emit` function. +2. Pass in the event payload required for the [`itemview`](https://docs.coveo.com/en/n9da0377#itemview) event. +No need to send the `meta` object, as it’s automatically handled by Relay. + +You can also use the Relay library directly to log view events on pages you want to be able to recommend but on which you don’t use Headless. +See the [Relay library documentation](https://docs.coveo.com/en/relay/latest/). \ No newline at end of file diff --git a/packages/headless/source_docs/headless-view-events.md b/packages/headless/source_docs/headless-view-events.md new file mode 100644 index 00000000000..c3e18840411 --- /dev/null +++ b/packages/headless/source_docs/headless-view-events.md @@ -0,0 +1,151 @@ +--- +title: Log view events with Coveo UA +group: Usage +category: Usage Analytics +slug: usage/usage-analytics/log-view-events with-coveo-ua +--- +# Log view events with Coveo UA + +You can use Headless controllers to handle Search API requests and leverage [Coveo Analytics](https://docs.coveo.com/en/182/). +Headless-powered search UI components can automatically log search and click [events](https://docs.coveo.com/en/260/) for user interactions in your [search interfaces](https://docs.coveo.com/en/2741/). +These events +provide the data to power most [Coveo Machine Learning (Coveo ML)](https://docs.coveo.com/en/188/) [models](https://docs.coveo.com/en/1012/), except for [Content Recommendations (CR)](https://docs.coveo.com/en/1016/). + +The output of a CR model depends on [view](https://docs.coveo.com/en/2949#view) events and the user’s action history. +Headless doesn’t +log these events for you, so you should ensure that you’re [sending view events](#send-view-events) for each page that you want to be able to recommend. + +
💡 TIP: Leading practice
+ +Start sending view events as soon as you can to gather data that your CR models can learn from. +
+ +## Send view events + +Use the [Coveo UA library](https://github.com/coveo/coveo.analytics.js) to send view events to Coveo UA. +You can load it from a [CDN link](#cdn) or install it as an [NPM package](#npm). + +### CDN + +The following code sample leverages the open source `coveoua.js` script to send a view event when a web page is loaded. + +```html + + + ① + + + PAGE_TITLE ② + + + + + + + +``` + +1. ``: The language identifier of the tracked page. +Coveo UA uses this value to populate the `language` metadata when sending view events. + +
❗ IMPORTANT
+ + Coveo ML models are split into distinct submodels for [each language](https://docs.coveo.com/en/1803#what-languages-do-coveo-ml-models-support), so you should set the `lang` HTML attribute for each page that you’re tracking. +
+2. `PAGE_TITLE`: The title of the tracked page. +Coveo UA uses this value to populate the `title` metadata when sending view events. +3. ``: A [public API key](https://docs.coveo.com/en/1718#api-key-templates) or a valid [search token](https://docs.coveo.com/en/1346/) if the page requires user authentication (see [Choose and implement a search authentication method](https://docs.coveo.com/en/1369/), [Search token authentication](https://docs.coveo.com/en/56/), [Execute queries domain](https://docs.coveo.com/en/1707#execute-queries-domain), and [Analytics data domain](https://docs.coveo.com/en/1707#analytics-data-domain)). + + ``: Your [organization analytics endpoint](https://docs.coveo.com/en/mcc80216#analytics-endpoint). + + * `https://.analytics.org.coveo.com` for a non-HIPAA organization + * `https://.analytics.orghipaa.coveo.com` for a HIPAA organization + + Where `` is the unique identifier of your Coveo organization. +4. The `send` command returns a promise. +To send multiple events sequentially, use `await`: + + **Example** + + ```javascript + async function sendEvents() { + try { + await coveoua("send", "view", { /* event data */ }); + await coveoua("send", "view", { /* event data */ }); + } catch (error) { + console.error("Error sending events:", error); + } + } + ``` +5. ``: The name of a [field](https://docs.coveo.com/en/1833/) that can be used to uniquely and permanently identify the tracked page as an [item](https://docs.coveo.com/en/210/) in the [index](https://docs.coveo.com/en/204/). +The `@clickableuri` field is a good choice for pages in a public website, because you can retrieve a web page’s URL using JavaScript code. +6. ``: The value of the `` field for the current tracked page. +If `` is set to `@clickableuri`, the `window.location.href` JavaScript function typically returns the matching `` for the current page. +7. ``: (Optional) The [type of content](https://docs.coveo.com/en/1744/) being tracked. +8. `CONTEXT_KEY`/``: (Optional) The [user context](https://docs.coveo.com/en/3389/) key-value pairs to pass for more personalized recommendations. +When you log view events with Coveo UA, all user context key names must be prefixed with `context_`. + + **Example** + + In your search interface, the users are authenticated and you wrote a `getUserRole` function to return the user role (`customer`, `employee`, or `partner`) from the profile of the current user performing the query. + Your custom context key is `userRole`, so you would pass it as follows when logging a view event: + + ```javascript + context_userRole: getUserRole(); + ``` + +
📌 Note
+ + If you’re passing user context key-values along with view events, you’ll likely want to ensure that your recommendation interface [does so as well](https://docs.coveo.com/en/1934#leverage-user-context-data-in-a-recommendation-interface) when it sends [queries](https://docs.coveo.com/en/231/). +
+ +### NPM + +The Coveo UA library is also available as an [npm package](https://www.npmjs.com/package/coveo.analytics). +You can install it with the following command: + +```bash +npm i coveo.analytics +``` + +You’ll want to have a module bundler (such as [webpack](https://webpack.js.org/)) installed and configured. + +The following code sample references the Coveo UA library as an ESM module and sends a view event when a web page is loaded. + +```javascript +import { CoveoAnalyticsClient } from 'coveo.analytics/modules'; +const ua = new CoveoAnalyticsClient({token: '', endpoint: ''}); ① +ua.sendViewEvent({ + contentIdKey: '', ② + contentIdValue: '', ③ + contentType: '' ④ +}); +``` + +1. ``: A [public API key](https://docs.coveo.com/en/1718#api-key-templates) or a valid [search token](https://docs.coveo.com/en/1346/) if the page requires user authentication (see [Choose and implement a search authentication method](https://docs.coveo.com/en/1369/), [Search token authentication](https://docs.coveo.com/en/56/), [Execute queries domain](https://docs.coveo.com/en/1707#execute-queries-domain), and [Analytics data domain](https://docs.coveo.com/en/1707#analytics-data-domain)). + + ``: Your [organization analytics endpoint](https://docs.coveo.com/en/mcc80216#analytics-endpoint). + + * `https://.analytics.org.coveo.com` for a non-HIPAA organization + * `https://.analytics.orghipaa.coveo.com` for a HIPAA organization + + Where `` is the unique identifier of your Coveo organization. +2. ``: The name of a [field](https://docs.coveo.com/en/1833/) that can be used to uniquely and permanently identify the tracked page as an [item](https://docs.coveo.com/en/210/) in the [index](https://docs.coveo.com/en/204/). +The `@clickableuri` field is a good choice for pages in a public site, because you can retrieve a web page’s URL using JavaScript code. +3. ``: The value of the `` field for the current tracked page. +If `` is set to `@clickableuri`, the `window.location.href` JavaScript function typically returns the matching `` for the current page. +4. ``: (Optional) The [type of content](https://docs.coveo.com/en/1744/) being tracked. \ No newline at end of file diff --git a/packages/headless/source_docs/highlighting.md b/packages/headless/source_docs/highlighting.md new file mode 100644 index 00000000000..2cda07674a7 --- /dev/null +++ b/packages/headless/source_docs/highlighting.md @@ -0,0 +1,186 @@ +--- +title: Implement highlighting +group: Usage +slug: usage/implement-highlighting +--- +# Implement highlighting +Coveo Headless offers highlighting for the following search elements: + +* [query suggestions](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchBox.html#state) +* [result list elements](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.Result.html): + * title + * excerpt + * printable URI + * first sentences + * summary + +This article explains how to implement highlighting in your Headless search interface. + +## Highlight Query Suggestions + +Let’s assume that you have a [`SearchBox`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchBox.html) component which offers clickable suggestions. + +Each time you add or delete a character in the search field, Headless performs a [`querySuggest`](https://docs.coveo.com/en/13#operation/querySuggestPost) request to the Search API. + +The response includes highlights and may look like this: + +```json +{ + "completions" : + [ + { + "expression" : "pdf", + "score" : 1019.0259590148926, + "highlighted" : "{pdf}", + "executableConfidence" : 1.0, + "objectId" : "20c05008-3afc-5f4e-9695-3ec326a5f745" + }, { + "expression" : "filetype pdf", + "score" : 19.025959014892578, + "highlighted" : "[filetype] {pdf}", + "executableConfidence" : 1.0, + "objectId" : "39089349-6e45-5c18-991c-7158143ec468" + } + ], + "responseId" : "9373a3c0-2c5c-4c9d-8adf-8bffd253ae3d" +} +``` + +To highlight query suggestions, update the target `SearchBox` controller instance as shown in the following code: + +```typescript +import {SearchBox as HeadlessSearchBox, buildSearchBox} from '@coveo/headless'; +import {headlessEngine} from '../engine'; +import {FunctionComponent, useEffect, useState} from 'react'; + +export const searchBox = buildSearchBox(headlessEngine, { ① + options: { + highlightOptions: { + notMatchDelimiters: { + open: '', + close: '', + }, + correctionDelimiters: { + open: '', + close: '', + }, + }, + }, +}); +interface SearchBoxProps { + controller: HeadlessSearchBox; +} + +const suggestionStyle = { + cursor: 'pointer', +}; + +export const SearchBox: FunctionComponent = (props) => { + const {controller} = props; + const [state, setState] = useState(controller.state); + + useEffect(() => controller.subscribe(() => setState(controller.state)), [ + controller, + ]); + + return ( +
+ controller.updateText(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && controller.submit()} + /> + {state.suggestions.length > 0 && ( ② +
    + {state.suggestions.map((suggestion) => { + return ( +
  • controller.selectSuggestion(suggestion.rawValue)} + dangerouslySetInnerHTML={{__html: suggestion.highlightedValue}} ③ + >
  • + ); + })} +
+ )} +
+ ); +}; +``` +1. Adds the highlighting delimiters during initialization of a `SearchBox` controller instance (see [`SuggestionHighlightingOptions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SuggestionHighlightingOptions.html)). +If you use valid HTML tags as shown in this example, Headless interprets them as tags rather than as regular text. +2. Adds a condition on showing a bulleted list of suggestions (if any). +3. Applies highlighting to the suggestions. +The highlighting depends on responses from the Search API. + +## Highlight Result Elements + +Highlighting result elements operates a bit differently, but it employs the same approach as with the `SearchBox` component. +You can specify what and how you want to highlight using the `highlightString` method. + +For example, you can highlight the excerpts in your result list as shown below. + +```typescript +import { + Result, + ResultList as HeadlessResultList, + buildResultList, + HighlightUtils, ① +} from '@coveo/headless'; +import {headlessEngine} from '../engine'; +import {FunctionComponent, useEffect, useState} from 'react'; + +export const resultList = buildResultList(headlessEngine); +interface ResultListProps { + controller: HeadlessResultList; +} + +export const ResultList: FunctionComponent = (props) => { + const {controller} = props; + const [state, setState] = useState(controller.state); + + useEffect(() => controller.subscribe(() => setState(controller.state)), [ + controller, + ]); + + if (!state.results.length) { + return
No results
; + } + + const highlightExcerpt = (result: Result) => { ② + return HighlightUtils.highlightString({ + content: result.excerpt, + highlights: result.excerptHighlights, + openingDelimiter: '', + closingDelimiter: '', + }); + }; + + return ( +
+
    + {state.results.map((result) => ( +
  • +
    +

    {result.title}

    +

    +
    +
  • + ))} +
+
+ ); +}; +``` +1. Imports the `HighlightUtils` module so you can use the `highlightString` method in your component. +2. Creates a custom function that returns the value of the `highlightString` method, which requires the following arguments: + * `content`, the element to highlight + * `highlights`, an object with highlights + * `openingDelimiter`, a string representing the opening delimiter + * `closingDelimiter`, a string representing the closing delimiter +3. Inserts an excerpt with highlighted strings right after the title. \ No newline at end of file diff --git a/packages/headless/source_docs/product-lifecycle.md b/packages/headless/source_docs/product-lifecycle.md new file mode 100644 index 00000000000..555315fdffe --- /dev/null +++ b/packages/headless/source_docs/product-lifecycle.md @@ -0,0 +1,51 @@ +--- +title: Product lifecycle +slug: product-lifecycle +--- +# Product lifecycle +This high velocity of innovation, combined with ever-changing dependencies (such as Node.js updates), means that certain legacy frameworks, methods, or features must be deprecated. + +This article describes the official product lifecycle for each version of the Headless library and its components. +Contractually, Coveo performs needed bug fixes for at least 18 months following the release of a major version. +We end support no earlier than 3 years after the initial release. +For more details, visit Coveo’s [version support lifecycle policy](https://docs.coveo.com/en/1485/). + +## Package versions + +The latest version of the Headless library can be downloaded from [npm](https://www.npmjs.com/package/@coveo/headless). +Links to older versions are also available, but support is limited as stated in the following table and as provided under the Coveo Support and Service-Level Policies. + +| Version number | Release date | Development end date | Bug fix end date | Support end date | +| --- | --- | --- | --- | --- | +| v3 | September 2024 | - | - | - | +| v2 | December 2022 | September 2024 | - | - | +| v1 | June 2021 | December 2022 | December 2022 | June 2024 | + +## Definitions + +### Development end date + +There are no more features or improvements for this version after the development end date. +New Headless releases after this date are no longer tested or supported with the listed package version or component. + +### Bug fix end date + +Major bug fixes are added to the latest official release of the version until the bug fix end date. +This documentation is no longer publicly searchable. + +### Support end date + +After the support end date, [Coveo Support](https://connect.coveo.com/s/case/Case/Default) will request that you upgrade to a supported version before offering any help. + +## 3rd-party support + +As of `@coveo/headless` v3.11.0, the following technologies versions are supported: + +* TypeScript v4.9+ +* Node.js v20 and v22 + +Support for newer versions of these technologies may be added in either major or minor updates. +Support for older versions may be removed in major updates. +The wrappers may come with limitations compared to the version of Atomic that uses native components. + +If the `peerDependencies` restrictions of one of these packages are more restrictive than those listed here, the `peerDependencies` restrictions are to be considered the source of truth. \ No newline at end of file diff --git a/packages/headless/source_docs/reference.md b/packages/headless/source_docs/reference.md new file mode 100644 index 00000000000..0feb38d6bcf --- /dev/null +++ b/packages/headless/source_docs/reference.md @@ -0,0 +1,6 @@ +--- +title: Reference +--- +# Reference documentation + +For React usage, see the [Headless-React reference documentation](https://docs.coveo.com/en/headless-react/latest/reference/index.html). \ No newline at end of file diff --git a/packages/headless/source_docs/result-templates.md b/packages/headless/source_docs/result-templates.md new file mode 100644 index 00000000000..b0b168ab3f8 --- /dev/null +++ b/packages/headless/source_docs/result-templates.md @@ -0,0 +1,247 @@ +--- +title: Use result templates +group: Usage +slug: usage/use-result-templates +--- +# Result templates +In Headless, a `ResultTemplate` is an object that specifies the content of a template as well as a list of conditions to fulfill for that template to be selected for any given result. + +**Example** + +```typescript +import { ResultTemplate, Result } from "@coveo/headless"; +import React from "React" + +const myResultTemplate: ResultTemplate = { ① + content: (result: Result) => (
{result.title}
), ② + conditions: [(result: Result) => { ③ + return !!result.title; + }] +} +``` +1. Defines a `ResultTemplate` element which will be rendered to the user interface. +Specifying the content and conditions properties is mandatory, whereas priority and fields are optional. +For more details on these properties, see the [reference documentation](https://docs.coveo.com/en/headless/latest/usage/result-templates#resulttemplate). +2. Creates a function that takes as input the given `result` and returns the `title`, enclosed in a `div` tag. +3. Specifies that the result must contain a `title` for this template to be used. + +As the complexity of your implementation increases, you may have to define multiple templates with different conditions registered by several components. +At this point, it may become difficult to manage the state of these templates. +Headless lets you create and manage templates with its `ResultTemplatesManager` controller to solve this issue. + +To use the `ResultTemplatesManager`, you must instantiate it and register result templates before executing a query. +When you render results, you can then call on the manager to select the most appropriate template for that result. + +The following example code shows a basic use case of the `ResultTemplatesManager`. + +```javascript +import { buildResultTemplatesManager, + ResultTemplatesManager, + Result, + ResultTemplatesHelpers } from "@coveo/headless"; +import { headlessEngine } from "../Engine"; +// ... +  +export default class ResultList { + // ... + private headlessResultTemplatesManager: ResultTemplatesManager; + // ... + constructor(props: any) { + // ... + this.headlessResultTemplatesManager = + buildResultTemplatesManager(headlessEngine); ① + this.headlessResultTemplatesManager.registerTemplates( ② + { + conditions: [], ③ + content: (result: Result) => ( +
  • +

    {result.title}

    +

    {result.excerpt}

    +
  • + ) + }, + { + conditions: [ + ResultTemplatesHelpers.fieldMustMatch("source", ["Techzample"])], ④ + content: (result: Result) => ( +
  • +

    Techzample: {result.title}

    +

    {result.excerpt}

    +
  • + ), + priority: 1 ⑤ + } + ); + // ... + } + // ... + render() { + return ( +
      + {this.state.results.map((result: Result) => { + const template = + this.headlessResultTemplatesManager.selectTemplate(result); ⑥ + return template(result); + })} +
    + ); + } +} +``` +1. Instantiates a new `ResultTemplatesManager`. +2. Registers templates on your `ResultTemplatesManager`. +You can pass multiple templates as separate parameters, or call `registerTemplates` multiple times. +3. Results will always satisfy this template’s conditions because there are none, making this the default template. +4. Headless offers result template helpers to make it easier to define conditions. +See the [reference documentation](https://docs.coveo.com/en/headless/latest/usage/result-templates#resulttemplateshelper) for more information. +5. Sets the priority of the result template. +If multiple templates' conditions are satisfied by a given result, the template with the highest priority will be selected. +If multiple templates have equal priority, the first template registered will win out. +6. Selects the most appropriate result template for the given result. + +## Reference + +Take a look at the [Headless project repository](https://github.com/coveo/ui-kit/tree/master/packages/headless/src/features/result-templates) for more information about the data types and methods detailed here. + +### ResultTemplate + +Element which will be rendered in the list of suggestions. + +| Property | Description | Type | +| --- | --- | --- | +| `content` (required) | The template itself. It can be anything, but generally it will be a function that takes a result and returns something to display. | `Content` | +| `conditions` (required) | The conditions that a result must satisfy for this template to be selected for that result. If an empty array of conditions is passed in, the template will be considered as a default that may be chosen for any result. Various [`ResultTemplatesHelpers`](https://docs.coveo.com/en/headless/latest/usage/result-templates#resulttemplateshelper) can easily be used to create conditions. | `ResultTemplateCondition[]` | +| `priority` | Defaults to 0. When `selectTemplate(result)` is called, the result may satisfy the conditions of several registered templates. When this happens, the template with the highest priority is selected. If several templates are satisfied and have the same priority, then the first one that was registered is chosen. | `number` | +| `fields` | The names of the Coveo fields used in the template’s content. This property tells the index to include those fields in the returned results. | `string[]` | + +### ResultTemplatesHelper + +Contains several helper methods to interact with a `ResultTemplate` object and define `conditions`. + +#### `getResultProperty` + +Extracts a property from a result object. + +**Parameters** + +* **result**: `Result` (required) + + The target result. +* **property**: `string` (required) + + The property to extract. + +**Returns** `unknown` ++ +The value of the specified property in the specified result, or null if the property does not exist. + +#### `fieldsMustBeDefined` + +Creates a condition that verifies if the specified fields are defined. + +**Parameters** + +* **fieldNames**: `string[]` (required) + + A list of fields that must be defined. + +**Returns** `ResultTemplateCondition` ++ +A function that takes a result and checks if every field in the specified list is defined. + +#### `fieldsMustNotBeDefined` + +Creates a condition that verifies if the specified fields are not defined. + +**Parameters** + +* **fieldNames**: `string[]` (required) + + A list of fields that must not be defined. + +**Returns** `ResultTemplateCondition` ++ +A function that takes a result and checks if every field in the specified list is not defined. + +#### `fieldMustMatch` + +Creates a condition that verifies if a field’s value contains any of the specified values. + +**Parameters** + +* **fieldName**: `string` (required) + + The name of the field to check. +* **valuesToMatch**: `string[]` (required) + + A list of possible values to match. + +**Returns** `ResultTemplateCondition` ++ +A function that takes a result and checks if the value for the specified field matches any value in the specified list. + +#### `fieldMustNotMatch` + +Creates a condition that verifies that a field’s value does not contain any of the specified values. + +**Parameters** + +* **fieldName**: `string` (required) + + The name of the field to check. +* **blacklistedValues**: `string[]` (required) + + A list of all disallowed values. + +**Returns** `ResultTemplateCondition` ++ +A function that takes a result and checks that the value for the specified field does not match any value in the given list. + +### ResultTemplatesManager + +Registers any number of result templates in the manager. + +#### Initialize + +##### `buildResultTemplatesManager` + +Build a manager where result templates can be registered and selected based on a list of conditions and priorities. + +**Parameters** + +* **engine**: `HeadlessEngine` (required) + + The `HeadlessEngine` instance of your application. + +**Returns** `ResultTemplatesManager` ++ +A new result templates manager. + +#### Methods + +##### `registerTemplates` + +Registers any number of result templates in the manager. + +**Parameters** + +* **templates**: `ResultTemplate[]` (required) + + The `HeadlessEngine` instance of your application. + +**Returns** `void` + +##### `selectTemplate` + +Selects the highest priority template for which the given result satisfies all conditions. +In the case where satisfied templates have equal priority, the template that was registered first is returned. + +**Parameters** + +* **result**: `Result` (required) + + The `HeadlessEngine` instance of your application. + +**Returns** `Content | null` ++ +The selected template’s content, or null if no template’s conditions are satisfied. \ No newline at end of file diff --git a/packages/headless/source_docs/server-side-rendering.md b/packages/headless/source_docs/server-side-rendering.md new file mode 100644 index 00000000000..72eee2c61bf --- /dev/null +++ b/packages/headless/source_docs/server-side-rendering.md @@ -0,0 +1,67 @@ +--- +title: Introduction +category: Server-side rendering +group: Usage +slug: usage/server-side-rendering/index +--- +# Server-side rendering +This approach is particularly useful when developing with the [Coveo Headless](https://docs.coveo.com/en/lcdf0493/) framework because it enables faster initial loading times and better SEO. +By rendering the HTML on the server, the client can receive a fully formed page which is ready to be displayed as soon as it’s received. + +The [`@coveo/headless-react`](https://www.npmjs.com/package/@coveo/headless-react) package includes utilities for [React](https://react.dev/) which are compatible with [Next.js](https://nextjs.org/) in the `@coveo/headless-react/ssr` sub-package. +These utilities enable SSR with the [Headless](https://docs.coveo.com/en/lcdf0493/) framework. + +
    📌 Note
    + +For a Coveo Commerce implementation, see [Headless for Commerce: Server-side rendering](https://docs.coveo.com/en/obif0156/). +
    + +## Prerequisites + +* You should know how to use [Headless](https://docs.coveo.com/en/lcdf0493/) engines and controllers. +You can refer to the [Headless](https://docs.coveo.com/en/lcdf0493/) [usage documentation](https://docs.coveo.com/en/headless/latest/usage/) for more information. +* You should also be familiar with React and Next.js. +Although you can read this documentation without an understanding of either framework, details which are specific to them won’t be explained. + +## Overview + +The goal is to achieve a general structure for SSR which involves executing a search and rendering the page on the server. +Then, on the client side, the page is [hydrated](https://en.wikipedia.org/wiki/Hydration_(web_development)), which means attaching interactivity and updating the page. + +The following sequence diagram illustrates the general process: + +![General server-side rendering sequence diagram](https://docs.coveo.com/en/assets/images/headless/general-ssr-sequence.png) + +## Server-side rendering and hydration with Headless + +The utilities for SSR use three core concepts: + +1. **Static state** + + This is the initial state of the application, which is generated on the server and sent to the client. + This initial state typically contains the data that’s required for the initial render of the app without interactivity, such as the initial state of each [Headless controller](https://docs.coveo.com/en/headless/latest/usage#use-headless-controllers). + + The static state is used to pre-populate (hydrate) the components with data. + This lets the app load with pre-rendered content on the first request, rather than waiting for the client-side rendering to fetch and display the data. +2. **Hydrated state** + + This is the state after it’s sent from the server to the client and then hydrated on the client side. + It contains [Headless controllers](https://docs.coveo.com/en/headless/latest/usage#use-headless-controllers) which you can use to interact with the state of the [Headless](https://docs.coveo.com/en/lcdf0493/) engine. + It also includes the [Headless](https://docs.coveo.com/en/lcdf0493/) engine. + + Synchronizing the state ensures that the behavior of the client-side application is consistent with what was initially rendered on the server. +3. **Engine definition** + + The engine definition specifies the configuration of the search engine. + This includes the list of controllers and their settings that should be included in the application. + + It returns methods to fetch the static state of the engine and to generate the hydrated state from a given static state. + The engine definition may also provide some additional utilities or helper functions that can be used in the application. + +The following updated sequence diagram illustrates how SSR and hydration are implemented with [Headless](https://docs.coveo.com/en/lcdf0493/): + +![Updated server-side rendering sequence diagram](https://docs.coveo.com/en/assets/images/headless/updated-ssr-sequence.png) + +## What's next? + +To implement server-side rendering in your [Headless](https://docs.coveo.com/en/lcdf0493/) [search interface](https://docs.coveo.com/en/2741/), refer to the [SSR usage documentation](https://docs.coveo.com/en/headless/latest/usage/headless-server-side-rendering/headless-implement-ssr/). \ No newline at end of file diff --git a/packages/headless/source_docs/ssr-extend-engine-definitons.md b/packages/headless/source_docs/ssr-extend-engine-definitons.md new file mode 100644 index 00000000000..4fa031299ff --- /dev/null +++ b/packages/headless/source_docs/ssr-extend-engine-definitons.md @@ -0,0 +1,233 @@ +--- +title: Extend engine definitions +group: Usage +category: Server-side rendering +slug: usage/server-side-rendering/extend-engine-definitions +--- +# Extend engine definitions + +```ts +const staticState = await engineDefinition.fetchStaticState(); +``` + +and you would hydrate it as follows: + +```ts +const hydratedState = await engineDefinition.hydrateStaticState({ + searchAction: staticState.searchAction, +}); +``` + +However, you may sometimes need to access the engine and controllers before the initial search is executed during `fetchStaticState`, or before the initial search gets simulated during `hydrateStaticState`. + +Coveo provides three utilities for these situations: + +* `build` +* `fetchStaticState.fromBuildResult` +* `hydrateStaticState.fromBuildResult` + +## Build the engine and controllers + +You can build an engine and its controllers directly from an engine definition by calling your engine definition’s `build` as follows: + +```ts +const { engine, controllers } = await engineDefinition.build(); +``` + +If you need to initialize the engine with a slightly different configuration than the one inherited from your engine definition, you may `extend` it as follows: + +```ts +const { engine, controllers } = await engineDefinition.build({ + extend(options) { + return { + ...options, + logFormatter() { + // ... + }, + }; + }, +}); +``` + +## Fetch the static state + +Internally, the `fetchStaticState` method does five things: + +1. It initializes an engine. +2. It initializes controllers. +3. It executes a search and waits for it to finish. +4. It returns `searchAction`. +This is an action which, when dispatched, is equivalent to re-executing the search and getting the same response. +5. It returns a copy of the state of every controller. + +If you want to access the engine and controllers, you can use `build`, which effectively replaces steps 1 and 2. +You can then use `fetchStaticState.fromBuildResult`, which effectively replaces steps 3 to 5. + +```ts +const { engine, controllers } = await engineDefinition.build(); +const staticState = await engineDefinition.fetchStaticState.fromBuildResult({ + buildResult: { engine, controllers }, +}); +``` + +## Hydrate the static state + +Internally, the `hydrateStaticState` method does four things: + +1. It initializes an engine. +2. It initializes controllers. +3. It dispatches the `searchAction` given to it. +4. It returns the engine and controllers it initialized. + +If you want to access the engine and controllers, you can use `build`, which effectively replaces steps 1 and 2. +You can then use `hydrateStaticState.fromBuildResult`, which effectively replaces steps 3 and 4. + +```ts +const { engine, controllers } = await engineDefinition.build(); +const staticState = await engineDefinition.hydrateStaticState.fromBuildResult({ + buildResult: { engine, controllers }, + searchAction, +}); +``` + +## Keep the server and client aligned + +If you choose to manipulate the engine or controllers before passing them to `fromBuildResult`, this could effect the state that’s returned by whichever `fromBuildResult` was called. +For this reason, we recommend that you extract any manipulations you do to your engine into a separate function. +You can then use it for both `fetchStaticState.fromBuildResult` and `hydrateStaticState.fromBuildResult`, as in the following code samples. + +In `common/engine-definition.ts`: + +```ts +import { + defineSearchEngine, + defineSearchBox, + defineResultList, + defineFacet, + getSampleSearchEngineConfiguration, + loadQueryActions, + SearchCompletedAction, +} from '@coveo/headless-react/ssr'; + +const engineDefinition = defineSearchEngine({ ① + configuration: getSampleSearchEngineConfiguration(), + controllers: { + searchBox: defineSearchBox(), + resultList: defineResultList(), + authorFacet: defineFacet({ field: "author" }), + sourceFacet: defineFacet({ field: "source" }), + }, +}); + +async function getBuildResult() { ② + const buildResult = await engineDefinition.build(); + const { updateQuery } = loadQueryActions(buildResult.engine); + buildResult.engine.dispatch(updateQuery({ q: "I like trains" })); + return buildResult; +} + +export async function fetchStaticState() { ③ + return engineDefinition.fetchStaticState.fromBuildResult({ + buildResult: await getBuildResult(), + }); +} + +export async function hydrateStaticState(options: { ④ + searchAction: SearchCompletedAction; +}) { + return engineDefinition.hydrateStaticState.fromBuildResult({ + buildResult: await getBuildResult(), + searchAction: options.searchAction, + }); +} +``` + +1. Don’t export `engineDefinition`. +2. Extract your logic to obtain the build result. +3. Export your own customized version of `fetchStaticState`. +4. Export your own customized version of `hydrateStaticState`. + +In `server.ts`: + +```ts +import { fetchStaticState } from './common/engine-definition.ts'; + +// ... + +const staticState = await fetchStaticState(); ① +// ... +``` + +1. Your server code isn’t polluted with with a lot of complex logic. + +In `client.ts`: + +```ts +import { hydrateStaticState } from './common/engine-definition.ts'; + +// ... + +const hydratedState = await hydrateStaticState({ + searchAction: staticState.searchAction, +}); +``` + +> [!WARNING] +> Avoid doing something like the following code samples. +> +> In `common/engine-definition.ts`: +> +> ```ts +> import { +> defineSearchEngine, +> defineSearchBox, +> defineResultList, +> defineFacet, +> getSampleSearchEngineConfiguration, +> } from '@coveo/headless-react/ssr'; +> +> export const engineDefinition = defineSearchEngine({ +> configuration: { +> ...getSampleSearchEngineConfiguration(), +> analytics: { enabled: false }, +> }, +> controllers: { +> searchBox: defineSearchBox(), +> resultList: defineResultList(), +> authorFacet: defineFacet({ field: "author" }), +> sourceFacet: defineFacet({ field: "source" }), +> }, +> }); +> ``` +> +> In `server.ts`: +> +> ```ts +> import { engineDefinition } from './common/engine-definition.ts'; +> import { loadQueryActions } from '@coveo/headless-react/ssr'; +> +> // ... +> +> const buildResult = await engineDefinition.build(); +> const { updateQuery } = loadQueryActions(buildResult.engine); +> buildResult.engine.dispatch(updateQuery({ q: "I like trains" })); +> +> const staticState = await engineDefinition.fetchStaticState.fromBuildResult({ +> buildResult, +> }); +> // ... +> ``` +> +> In `client.ts`: +> +> ```ts +> import { engineDefinition } from './common/engine-definition.ts'; +> +> const buildResult = await engineDefinition.build(); +> const hydratedState = await engineDefinition.hydrateStaticState.fromBuildResult( +> { +> buildResult, +> searchAction: staticState.searchAction, +> } +> ); +> ``` \ No newline at end of file diff --git a/packages/headless/source_docs/ssr-implement-search-parameter-support.md b/packages/headless/source_docs/ssr-implement-search-parameter-support.md new file mode 100644 index 00000000000..ab2ec7b2e57 --- /dev/null +++ b/packages/headless/source_docs/ssr-implement-search-parameter-support.md @@ -0,0 +1,208 @@ +--- +title: Implement search parameter support +group: Usage +category: Server-side rendering +slug: usage/server-side-rendering/implement-search-parameter-support +--- +# Implement search parameter support + +We recommend that you use the [Coveo Headless](https://docs.coveo.com/en/lcdf0493/) SSR utilities with the latest [Next.js](https://nextjs.org/) [App Router](https://nextjs.org/docs/app). +We don’t fully support the [Pages Router](https://nextjs.org/docs/pages). +This article uses the App Router paradigm. + +If you decide to use the Pages Router paradigm, there are potential issues which might lead to server reruns during client-side navigation. +This would cause both the client and server to execute search requests to the [Coveo Platform](https://docs.coveo.com/en/186/). +For more details on the root cause of this behavior, refer to [this GitHub issue](https://github.com/vercel/next.js/discussions/19611). + +
    💡 TIP
    + +Although you could read this article without being familiar with Next.js, we recommend that you follow the Next.js [Getting Started](https://nextjs.org/docs) documentation first. +
    + +## Define the engine and controllers + +You need to define the engine and controllers required for the search page. +This involves setting up the `SearchParameterManager` component, which is responsible for managing the state of the [search interface](https://docs.coveo.com/en/2741/) and synchronizing it with the URL. + +In `src/engine.ts`: + +```ts +import { + defineSearchEngine, + defineSearchParameterManager, +} from '@coveo/headless-react/ssr'; + +const accessToken = ""; +const organizationId = ""; +const engineDefinition = defineSearchEngine({ + configuration: { + accessToken, + organizationId, + }, + // ... Any other Headless controller that you want to add to the page (such as Facet or Result list) + controllers: { + // ... Other controllers + searchParameterManager: defineSearchParameterManager() ① + }, +}); + +export const { useSearchParameterManager } = engineDefinition.controllers; ② +``` + +1. `searchParameterManager` must be added as the last controller, because it requires other controllers to be initialized first. +Controllers are built in the order in which they’re specified in the engine definition. +2. Make sure to export the `useSearchParameterManager` hook, which will synchronize the search parameters with the URL. + +## Define the component + +The `SearchParameterManager` component uses the `useSearchParameterManager` hook (exposed in `src/engine.ts`) and the `useAppHistoryRouter` [custom React hook](https://react.dev/learn/reusing-logic-with-custom-hooks) to manage browser history and URL updates. + +Create a new file (`src/components/search-parameter-manager.tsx`) and define the `SearchParameterManager` component as follows: + +```ts +'use client'; + +import { useAppHistoryRouter } from './history-router'; +import { useSearchParameterManager } from '../engine'; + +export default function SearchParameterManager() { + const historyRouter = useAppHistoryRouter(); + const {state, methods} = useSearchParameterManager(); + + // ... Rendering logic + + return <>; +} +``` + +### Implement a history router hook + +The following example shows how you can implement a custom hook to manage browser history. +Add the code in `src/components/history-router.tsx`: + +```ts +'use client'; + +import { useEffect, useMemo, useCallback } from 'react'; + +function getUrl() { + if (typeof window === 'undefined') { + return null; + } + return new URL(document.location.href); +} + +export function useAppHistoryRouter() { + const [url, updateUrl] = useReducer(() => getUrl(), getUrl()); + useEffect(() => { + window.addEventListener('popstate', updateUrl); + return () => window.removeEventListener('popstate', updateUrl); + }, []); + const replace = useCallback( + (href: string) => window.history.replaceState(null, document.title, href), + [] + ); + const push = useCallback( + (href: string) => window.history.pushState(null, document.title, href), + [] + ); + return useMemo(() => ({url, replace, push}), [url, replace, push]); +} +``` + +### Update the UI when the URL changes + +The `SearchParameterManager` component uses the [`useEffect`](https://react.dev/reference/react/useEffect) hook to synchronize the [search interface](https://docs.coveo.com/en/2741/) with the current URL whenever its search parameter changes. + +
    💡 TIP
    + +For more info about `useEffect`, see [Synchronizing with Effects](https://react.dev/learn/synchronizing-with-effects). +
    + +Any search filter (such as [facet](https://docs.coveo.com/en/198/) value, [query](https://docs.coveo.com/en/231/), or sort criteria) in the URL will automatically be reflected in the interface when loading the search page with specific parameters in the URL. +Add the following code in `src/components/search-parameter-manager.tsx`: + +```ts +import { buildSSRSearchParameterSerializer } from '@coveo/headless/ssr'; +import { useEffect, useMemo } from 'react'; + +// ... + + useEffect(() => ( + methods && + historyRouter.url?.searchParams && + methods.synchronize( + buildSSRSearchParameterSerializer().toSearchParameters(historyRouter.url.searchParams) ① + ) + ), [historyRouter.url?.searchParams, methods]); +``` + +1. The `buildSSRSearchParameterSerializer.toSearchParameters` utility reads search parameters from the URL and parses them into an object that can be added to the [Coveo Platform](https://docs.coveo.com/en/186/) state. + +### Update the URL when the UI changes + +The browser’s URL also needs to be updated whenever there’s a state change from the [search interface](https://docs.coveo.com/en/2741/). +Add the following code in `src/components/search-parameter-manager.tsx`: + +```ts +const correctedUrl = useMemo(() => { ① + if (!historyRouter.url) { + return null; + } + + const newURL = new URL(historyRouter.url); + const { serialize } = buildSSRSearchParameterSerializer(); + + return serialize(state.parameters, newURL); +}, [state.parameters]); + +useEffect(() => { ② + if (!correctedUrl || document.location.href === correctedUrl) { + return; + } + + const isStaticState = methods === undefined; + historyRouter[isStaticState ? 'replace' : 'push'](correctedUrl); +}, [correctedUrl, methods]); +``` + +1. The [`useMemo`](https://react.dev/reference/react/useMemo) hook listens for any parameter changes in the state. +Whenever there’s a change, the state’s parameters are serialized (using the `serialize` utility provided by the `@coveo/headless/ssr` package) and applied to the URL. +2. The `useEffect` hook then updates the browser’s history state. + +
    📌 Note
    + +You can consult a [working demo](https://github.com/coveo/ui-kit/tree/master/samples/headless-ssr/search-nextjs/app-router) of the component. +
    + +## Add the component to the search page + +```ts +// page.tsx + +import { SearchParameterManager } from './components/search-parameter-manager'; +import { SearchPageProvider } from '...'; +import { fetchStaticState } from './engine'; +import { buildSSRSearchParameterSerializer } from '@coveo/headless-react/ssr'; + +export default async function Search({searchParams}) { + const {toSearchParameters} = buildSSRSearchParameterSerializer(); + const searchParameters = toSearchParameters(searchParams); + + const staticState = await fetchStaticState({ + controllers: { + searchParameterManager: { + initialState: {parameters: searchParameters}, + }, + }, + }); + + return ( + + + + ); +} + +export const dynamic = 'force-dynamic'; +``` \ No newline at end of file diff --git a/packages/headless/source_docs/ssr-send-context.md b/packages/headless/source_docs/ssr-send-context.md new file mode 100644 index 00000000000..1b838007cd2 --- /dev/null +++ b/packages/headless/source_docs/ssr-send-context.md @@ -0,0 +1,98 @@ +--- +title: Send context +group: Usage +category: Server-side rendering +slug: usage/server-side-rendering/send-context +--- +# Send context + +For both of the following examples, assume that there’s a shared configuration file (`engine.ts`) which defines the search engine and context controller: + +```ts +// engine.ts + +import { defineSearchEngine, defineContext } from '@coveo/headless-react/ssr'; + +const engineDefinition = defineSearchEngine({ + // ... + controllers: { context: defineContext() }, +}); + +export const { fetchStaticState } = engineDefinition; +``` + +## Send context from the server before the page first loads + +You need to define the context values on the server before the page first loads so that they can be added to future requests. + +The following is an example implementation: + +```tsx +// server.ts + +import { fetchStaticState } from './engine.ts'; + +export default async function Search() { + const contextValues = { + region: 'Canada', + role: getRole(), + }; + + const staticState = await fetchStaticState({ + controllers: { + context: { + initialState: { values: contextValues }, + }, + }, + }); + + return ( + + {/* Other search page components */} + + ); +} +``` + +Once your application is hydrated, the initial context values will persist on the client side. + +## Send context from the client side + +You can use the [Coveo Headless](https://docs.coveo.com/en/lcdf0493/) `useContext` method to send context from the client side. +This method lets you access the context controller and set the context values. + +First, modify the `engine.ts` file to export the `useContext` hook: + +```ts +// engine.ts + +// ... +export const { useContext } = engineDefinition.controllers; +``` + +Then, in your component, you can use the `useContext` and `set` methods to set the context values: + +```tsx +// component/context.ts + +'use client'; + +import { useContext } from '../path/to/engine.ts'; + +export default function Context() { + const { methods } = useContext(); + + methods?.set({ + region: 'Canada', + role: getRole() + }) + + return <> +} +``` + +
    ❗ IMPORTANT + +Don’t forget to add this component inside your `SearchPageProvider`. + +
    \ No newline at end of file diff --git a/packages/headless/source_docs/ssr-usage.md b/packages/headless/source_docs/ssr-usage.md new file mode 100644 index 00000000000..debf2af4846 --- /dev/null +++ b/packages/headless/source_docs/ssr-usage.md @@ -0,0 +1,252 @@ +--- +title: Implement server-side rendering +group: Usage +category: Server-side rendering +slug: usage/server-side-rendering/implement-server-side-rendering +--- +# Implement server-side rendering + +We recommend using the [Coveo Headless](https://docs.coveo.com/en/lcdf0493/) SSR utilities with the latest [Next.js](https://nextjs.org/) [App Router](https://nextjs.org/docs/app). +We don’t fully support the [Pages Router](https://nextjs.org/docs/pages) and using it may result in unexpected behavior. +This article uses the App Router paradigm. + +**Notes** + +* You can also consult a [working demo](https://github.com/coveo/ui-kit/tree/master/samples/headless-ssr/search-nextjs/app-router) of a [Headless](https://docs.coveo.com/en/lcdf0493/) SSR search page. +* For a Coveo Commerce implementation, see [Headless for Commerce: Server-side rendering](https://docs.coveo.com/en/obif0156/). + +## Overview + +The strategy for implementing SSR in [Headless](https://docs.coveo.com/en/lcdf0493/) is as follows: + +* In a shared file: Create and export an engine definition. +* On the server: + 1. Fetch the static state. + 2. Wrap your components or page with a `StaticStateProvider` and render it. + 3. Send the page and static state to the client. +* On the client: + 1. Fetch the hydrated state. + 2. Replace the `StaticStateProvider` with a `HydratedStateProvider` and render it. + +## Create an engine definition + +1. Create and export an engine definition in a shared file. +It should include the controllers, their settings, and the search engine configuration, as in the following example: + + ```ts + import { + defineSearchEngine, + defineSearchBox, + defineResultList, + defineFacet, + getSampleSearchEngineConfiguration, + } from '@coveo/headless-react/ssr'; + + export const engineDefinition = defineSearchEngine({ + configuration: { + ...getSampleSearchEngineConfiguration(), + analytics: { enabled: false }, + }, + controllers: { + searchBox: defineSearchBox(), + resultList: defineResultList(), + authorFacet: defineFacet({ options: { field: "author" } }), + sourceFacet: defineFacet({ options: { field: "source" } }), + }, + }); + ``` +2. Fetch the static state on the server side using your engine definition, as in the following example: + + ```ts + const staticState = await engineDefinition.fetchStaticState(); + // ... Render your UI using the `staticState`. + ``` +3. Fetch the hydrated state on the client side using your engine definition and the static state, as in the following example: + + ```ts + 'use client'; + // ... + + const hydratedState = await engineDefinition.hydrateStaticState({ + searchAction: staticState.searchAction, + }); + // ... Update your UI using the `hydratedState`. + ``` + +Once you have the hydrated state, you can add interactivity to the page. + +## Build the UI components + +Engine definitions contain [hooks](https://react.dev/reference/react) and [context providers](https://react.dev/reference/react/createContext#provider) to help build your UI components. + +### Use hooks in your UI components + +Engine definitions contain different kinds of hooks for React and Next.js. + +1. Controller hooks: + * For each controller the definition was configured with, a corresponding hook exists which returns + * The state of its corresponding controller. + * The methods of its corresponding controller. + * Each controller hook will automatically re-render the component in which it was called whenever the state of its controller is updated. +2. The `useEngine` hook: + * Returns an engine. + +The following is an example of how you would build [facet](https://docs.coveo.com/en/198/) components for the same engine definition used in the previous examples. + +
    ❗ IMPORTANT
    + +If you’re using Next.js with the [App Router](https://nextjs.org/docs/app), any file which uses these hooks must begin with the [’use client'`](https://nextjs.org/docs/app/building-your-application/rendering/client-components#using-client-components-in-nextjs) directive. +
    + +```tsx +'use client'; + +import { engineDefinition } from '...'; + +const { useAuthorFacet, useSourceFacet } = engineDefinition.controllers; ① + +export function AuthorFacet() { + const { state, methods } = useAuthorFacet(); + + return ; +} + +export function SourceFacet() { + const { state, methods } = useSourceFacet(); + + return ; +} + +function BaseFacet({ + state, + methods, +}: ReturnType) { + // ... Rendering logic +} +``` + +1. Extract the utilities that you need from the engine definition. +The `useAuthorFacet` and `useSourceFacet` hooks are automatically generated by [Headless](https://docs.coveo.com/en/lcdf0493/). +They’re named after the `authorFacet` and `sourceFacet` controller map entries, but are automatically capitalized and prefixed with "use" by [Headless](https://docs.coveo.com/en/lcdf0493/). + +### Provide the static or hydrated state to the hooks + +To use hooks in your UI components, these UI components must be wrapped with one of the context providers contained in their corresponding engine definition. + +1. The `StaticStateProvider`: + * Takes a static state as a prop. + * Provides controller hooks' states with controller states. + * Provides controller hooks' methods with `undefined`. + * Provides the `useEngine` hook with `undefined`. +2. The `HydratedStateProvider`: + * Takes a hydrated state as a prop. + * Provides controller hooks' states with controller states. + * Provides controller hooks' methods with controller methods. + * Provides the `useEngine` hook with an engine. + +Using these new providers, we have the necessary components to complete the full loop and implement SSR. + +The following example demonstrates how to replace the `StaticStateProvider` with the `HydratedStateProvider` once you have the hydrated state, by making a custom component that takes on the responsibility of hydration and choosing the provider. + +
    ❗ IMPORTANT
    + +If you’re using Next.js with the [App Router](https://nextjs.org/docs/app), any file which uses these hooks must begin with the [’use client'`](https://nextjs.org/docs/app/building-your-application/rendering/client-components#using-client-components-in-nextjs) directive. +
    + +```tsx +'use client'; + +import { useEffect, useState, PropsWithChildren } from 'react'; +import { engineDefinition } from '...'; +import { + InferStaticState, + InferHydratedState, +} from '@coveo/headless-react/ssr'; + +const { hydrateStaticState, StaticStateProvider, HydratedStateProvider } = + engineDefinition; ① + +type StaticState = InferStaticState; +type HydratedState = InferHydratedState; ② + +export function EngineStateProvider({ + staticState, + children, +}: PropsWithChildren<{ staticState: StaticState }>) { + const [hydratedState, setHydratedState] = useState( ③ + null + ); + + useEffect(() => { ④ + hydrateStaticState({ + searchAction: staticState.searchAction, + }).then(setHydratedState); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + if (!hydratedState) { ⑤ + return ( + + {children} + + ); + } + + return ( ⑥ + + {children} + + ); +} +``` + +1. Extract the utilities that you need from the engine definition. +2. Declare the `StaticState` and `HydratedState` to improve readability. +3. Use [`useState`](https://react.dev/reference/react/useState) to allow switching between `no hydrated state` or `rendering on the server` and `hydration completed`. +Here, `null` means that either hydration isn’t complete or it’s currently rendering on the server. +In both cases, you’ll want to render the static state. +4. Use [`useEffect`](https://react.dev/reference/react/useEffect) to only hydrate on the client side. +5. Render the `StaticStateProvider` until hydration has completed. +6. When hydration is finished, replace the `StaticStateProvider` with the `HydratedStateProvider`. + +Here’s an example of how you would use this component in a Next.js App Router [page](https://nextjs.org/docs/pages/building-your-application/routing/pages-and-layouts): + +```tsx +import { engineDefinition } from '...'; +import { SearchPageProvider } from '...'; +import { ResultList } from '...'; +import { SearchBox } from '...'; +import { AuthorFacet, SourceFacet } from '...'; + +const { fetchStaticState } = engineDefinition; ① + +export default async function Search() { ② + const staticState = await fetchStaticState({ + controllers: {/*...*/}, + }); + + return ( + + + + + + + ); +} +``` + +1. Extract the utilities that you need from the engine definition. +2. Anything inside this function will only be executed on the server. +See [Data Fetching](https://nextjs.org/docs/app/building-your-application/data-fetching) for more information. + +## What's next? + +For more advanced use cases, such as dispatching actions or interacting with the engine on the server side, refer to the following articles: + +* [Extend engine definitions](https://docs.coveo.com/en/headless/latest/usage/headless-server-side-rendering/extend-engine-definitions/) +* [Implement search parameter support](https://docs.coveo.com/en/headless/latest/usage/headless-server-side-rendering/implement-search-parameter-support/) +* [Send context](https://docs.coveo.com/en/headless/latest/usage/headless-server-side-rendering/ssr-send-context/) \ No newline at end of file diff --git a/packages/headless/source_docs/standalone-search-box.md b/packages/headless/source_docs/standalone-search-box.md new file mode 100644 index 00000000000..86c2f821463 --- /dev/null +++ b/packages/headless/source_docs/standalone-search-box.md @@ -0,0 +1,101 @@ +--- +title: Use a standalone search box +group: Usage +slug: usage/use-a-standalone-search-box +--- +# Use a standalone search box +As an example, take the search box at the top of this page. +It allows you to start your search while on this page before redirecting you to the full search page. +Because such a search experience involves two pages, some data needs to be shared between them so that the query response is correct, and so that you log the proper [Coveo Analytics events](https://docs.coveo.com/en/260/). + +This article walks you through the implementation of such a search experience. + +## Create the page with the standalone search box + +If you’ve already worked with [Coveo Headless controllers](https://docs.coveo.com/en/headless/latest/usage#use-headless-controllers), this step should already be familiar to you. +You need to create a search engine, instantiate a standalone search box controller and connect it to the search box DOM element. +A React example implementation is available [here](https://github.com/coveo/ui-kit/blob/master/packages/samples/headless-react/src/components/standalone-search-box/standalone-search-box.fn.tsx). + +## Communicate between the two pages + +When a visitor makes a search request using the standalone search box, the query and analytics data need to be stored before redirecting the user, to ensure that the full search page receives this stored query and analytics data. +You should use [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) as the browser storage mechanism, because the [client ID](https://docs.coveo.com/en/masb0234/) used by [Coveo Analytics](https://docs.coveo.com/en/182/) is kept in local storage. +For example, in the page with the standalone search box, you would include the following: + +```typescript +searchBox.subscribe(() => { + const {redirectTo, value, analytics} = searchBox.state; +​ + if (redirectTo) { + const data = {value, analytics}; + localStorage.setItem('coveo_standalone_search_box_data', JSON.stringify(data)); +​ + // perform redirect + window.location.href = redirectTo; + } +}) +``` + +## Create the full search page + +Prior to executing the first search on the full search page, you should configure `[originLevel3](https://docs.coveo.com/en/1339/)` so that your queries are logged correctly. +You also need to set the correct query. + +You can configure `originLevel3` using the `document.referrer` value when initializing the engine, as shown below: + +```typescript +import { buildSearchEngine } from '@coveo/headless'; +​ +const engine = buildSearchEngine({ + configuration: { + // ... + analytics: { + originLevel3: document.referrer, + }, + }, +}); +``` + +In your full search page, you can set the query using the [`updateQuery`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.QueryActionCreators.html#updateQuery) action, as shown below: + +```typescript +import {loadQueryActions} from '@coveo/headless'; +​ +const {updateQuery} = loadQueryActions(engine); +const data = localStorage.getItem('coveo_standalone_search_box_data'); +const {value} = JSON.parse(data); +​ +engine.dispatch(updateQuery({q: value})); +``` + +The final steps are to delete the data in local storage and to handle the case where someone reaches the search interface without using the standalone search box (in which case there’s no data in `localstorage`). + +If you do all of this, here’s what the code for your full search page will look like: + +```typescript +import {buildSearchEngine, loadQueryActions, loadSearchAnalyticsActions} from '@coveo/headless'; +​ +const engine = buildSearchEngine({ + configuration: { + // ... + analytics: { + originLevel3: document.referrer, + }, + }, +}); +​ +const {updateQuery} = loadQueryActions(engine); +const data = localStorage.getItem('coveo_standalone_search_box_data'); +​ +if (data) { + localStorage.removeItem('coveo_standalone_search_box_data'); + const {value, analytics} = JSON.parse(data); +​ engine.dispatch(updateQuery({q: value})); + engine.executeFirstSearchAfterStandaloneSearchBoxRedirect(analytics); +} else { + engine.executeFirstSearch(); +} +``` + +There you go! +If you’ve made it this far, you’ve set up a standalone search box that’s sending the necessary analytics events, which can be leveraged by reporting [dashboards](https://docs.coveo.com/en/256/) and [Coveo Machine Learning (Coveo ML)](https://docs.coveo.com/en/188/). \ No newline at end of file diff --git a/packages/headless/source_docs/synchronize-search-parameters-with-the-url.md b/packages/headless/source_docs/synchronize-search-parameters-with-the-url.md new file mode 100644 index 00000000000..91f913a572d --- /dev/null +++ b/packages/headless/source_docs/synchronize-search-parameters-with-the-url.md @@ -0,0 +1,103 @@ +--- +title: Synchronize search parameters with the URL +group: Usage +slug: usage/synchronize-search-parameters-with-the-url +--- +# Synchronize search parameters with the URL +Headless provides two controllers to help you keep the URL of your application in sync with your [Headless engine](https://docs.coveo.com/en/headless/latest/usage#configure-a-headless-engine) state. +This article explains how to use those controllers, and why you would choose one over the other. + +
    📌 Note
    + +The context where you would make use of those controllers is during the initialization of your Headless application, so you may want to [review how to do so](https://docs.coveo.com/en/headless/latest/usage#initialize-your-interface) before reading this article. +
    + +## `buildUrlManager` + +The [`UrlManager`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.UrlManager.html) controller is the faster option, taking a few minutes to set up. +It monitors the search parameters affecting the result list, and serializes them into a URL-ready string that contains values such as the query, sort criteria and selected facet values. + +**Example** + +Your search page is in a state where the query is "hello" and where the results are sorted by descending date. +The `UrlManager` would serialize this state as the following string: `q=hello&sortCriteria=date%20descending`. + +The `UrlManager` controller can also do the reverse, that is, deserialize a string and update the Headless engine state values accordingly. +However, because the controller manages serialization and deserialization, it doesn’t offer control over the serialization form of the URL. + +```javascript +const fragment = window.location.hash.slice(1); +const urlManager = buildUrlManager(engine, { ① + initialState: {fragment} +}) +  +urlManager.subscribe(() => { ② + const hash = `#${this.urlManager.state.fragment}`; + + if (!this.searchStatus.state.firstSearchExecuted) { ③ + history.replaceState(null, document.title, hash); + + return; + } + + history.pushState(null, document.title, hash); +}); +  +window.addEventListener('hashchange', () => { ④ + const fragment = window.location.hash.slice(1); + urlManager.synchronize(fragment); +}); +``` +1. Set the initial search parameters to the values in the URL when a page first loads. +2. Update the hash when search parameters change. +3. Using `replaceState()` instead of `pushState()` in this case ensures that the URL reflects the current state of the search page on the first interface load. +If `pushState()` were used instead, users could possibly enter a history loop, having to click the back button multiple times without being able to return to a previous page. +This situation happens with components such as the `Tab` component, which adds a new state to the browser history stack. +Using `replaceState` instead replaces the current state of the browser history with a new state, effectively updating the URL without adding a new entry to the history stack. +4. Update the search parameters when an end user manually changes the hash. + +## `buildSearchParameterManager` + +The [`SearchParameterManager`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchParameterManager.html) controller provides the active search parameters as an object rather than a string, giving you full control over how to serialize them. +Conversely, the controller can take in an object with search parameters with which to update the Headless engine state. +The following sample is similar to the one above, but uses custom `serialize` and `deserialize` functions you would define yourself: + +```javascript +const url = getUrl(); +const parameters = deserialize(url); +const manager = buildSearchParameterManager(engine, { ① + initialState: {parameters} +}) +  +manager.subscribe(() => { ② + const url = serialize(manager.state.parameters); + if (!this.searchStatus.state.firstSearchExecuted) { ③ + history.replaceState(null, document.title, hash); + + return; + } + history.pushState(null, document.title, url); +}); +  +window.addEventListener('hashchange', () => { ④ + const url = getUrl(); + const parameters = deserialize(url); + manager.synchronize(parameters); +}); +  +function serialize(parameters) { + // ... +} +  +function deserialize(url) { + // ... +} +``` +1. Set the initial search parameters to the values in the URL when a page first loads. +2. Update the URL when search parameters change. +3. This condition handles cases that could cause bugs in some interfaces. +4. Update the search parameters when a user manually changes the URL. + +In summary, Headless offers two controllers to synchronize your engine state with the URL. +The `buildUrlManager` controller is faster to set up, but doesn’t let you control the form of the URL. +The `buildSearchParameterManager` controller offers full control over the form of the URL, but takes more time to set up since it does not handle search parameter serialization and deserialization. \ No newline at end of file diff --git a/packages/headless/source_docs/upgrade-from-v1-to-v2.md b/packages/headless/source_docs/upgrade-from-v1-to-v2.md new file mode 100644 index 00000000000..2abe7b450e1 --- /dev/null +++ b/packages/headless/source_docs/upgrade-from-v1-to-v2.md @@ -0,0 +1,598 @@ +--- +title: v1 to v2 +group: Upgrade +slug: upgrade/v1-to-v2 +--- +# Upgrade from v1 to v2 + +
    ❗ IMPORTANT: The following are breaking changes from Headless v1 to v2
    + +
    + +## Renamed variables + +The following elements were renamed without changes to their underlying functionality. + +* `RelevanceInspector.fetchFieldDescriptions` renamed to `RelevanceInspector.fetchFieldsDescription` + + **Headless Version 1** + + ```js + + ``` + + **Headless Version 2** + + ```js + + ``` + + Documentation: [RelevanceInspector](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.RelevanceInspector.html) +* `RelevanceInspectorState.fieldDescriptions` renamed to `RelevanceInspectorState.fieldDescription` + + Documentation: [RelevanceInspector](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.RelevanceInspector.html) +* `ResultListState.searchUid` renamed to `ResultListState.searchResponseId` + + Documentation: [ResultList](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ResultList.html) +* `searchAPIClient` renamed to `apiClient` + + Documentation: [SearchActions](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchActionCreators.html) + +## Changes + +### CDN + +https://static.cloud.coveo.com/headless/latest/* resources will no longer be updated (also, using this wasn’t recommended). +If you want to use the CDN instead of [using npm to install Headless](https://docs.coveo.com/en/headless/latest/usage#install-headless), specify a major version to follow updates, such as in https://static.cloud.coveo.com/headless/v2/*. + +**Headless Version 1** + +```html + +``` + +or + +```html + +``` + +**Headless Version 2** + +```html + +``` + +### [`FieldSuggestionsOptions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.FieldSuggestionsOptions.html) + +Except for the `delimitingCharacter` option, which had no effect and has been removed completely, the options exposed through `FieldSuggestionsOptions` have been removed. +They must now be set by passing a [`FacetOptions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.FacetOptions.html) object. + +**Headless Version 1 Example** + +```ts +controller = buildFieldSuggestions(engine, {field: 'author', facetId: 'author-2'}); +``` + +**Headless Version 2 Example** + +```ts +controller = buildFieldSuggestions(engine, {facet: {field: 'author', facetId: 'author-2'}}); +``` + +Similarly, the `FieldSuggestionsFacetSearchOptions` have been removed and you must use `FacetSearchOptions` instead. +Because they expose the same attributes, this change should be transparent. + +**Headless Version 1 Example** + +```ts +controller = buildFieldSuggestions(engine, {field: 'author', facetId: 'author-2', facetSearch: {query: "herman"}}); +``` + +**Headless Version 2 Example** + +```ts +controller = buildFieldSuggestions(engine, {facet: {field: 'author', facetId: 'author-2', facetSearch: {query: "herman"}}}); +``` + +### [`CategoryFieldSuggestionsOptions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.CategoryFieldSuggestionsOptions.html) + +Except for the `delimitingCharacter` option, which had no effect and has been removed completely, the options exposed through `CategoryFieldSuggestionsOptions` have been removed and must now be set by passing a [`CategoryFacetOptions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.CategoryFacetOptions.html) object. + +**Headless Version 1 Example** + +```ts +controller = buildCategoryFieldSuggestions(engine, { + options: { + field: 'geographicalhierarchy', + facetId: 'geographicalhierarchy-3', + } +}); +``` + +**Headless Version 2 Example** + +```ts +controller = buildCategoryFieldSuggestions(engine, { + options: { + facet: { + field: 'geographicalhierarchy', + facetId: 'geographicalhierarchy-3', + } + } +}); +``` + +Similarly, the `CategoryFieldSuggestionsFacetSearchOptions` have been removed and you must use `CategoryFacetSearchOptions` instead. +Because they expose the same attributes, this change should be transparent. + +**Headless Version 1 Example** + +```ts +controller = buildCategoryFieldSuggestions(engine, { + options: { + field: 'geographicalhierarchy', + facetId: 'geographicalhierarchy-3', + facetSearch: {query: "brazil"} + } +}); +``` + +**Headless Version 2 Example** + +```ts +controller = buildCategoryFieldSuggestions(engine, { + options: { + facet: { + field: 'geographicalhierarchy', + facetId: 'geographicalhierarchy-3', + facetSearch: {query: "brazil"} + } + } +}); +``` + +### [`UrlManager`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.UrlManager.html) + +The `UrlManager` now serializes facets with a dash instead of brackets. + +For instance, `f[my-facet]=valueA,valueB` in v1 is serialized as `f-my-facet=valueA,valueB` in v2. + +The `UrlManager` generally handles this part of the serialization and deserialization by itself, so the changes should be transparent, unless you were leveraging the URL directly somehow, for example by linking to your search page with preselected facets. +In that case, adjust to the new serialization. + +**Headless Version 1 Example** + +```html + +``` + +**Headless Version 2 Example** + +```html + +``` + +### [`BreadcrumbActionCreator`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.BreadcrumbActionCreators.html) + +`deselectAllFacets` has been removed. +Instead, use `deselectAllBreadcrumbs`. + +**Headless Version 1 Example** + +```ts +import { engine } from './engine'; +import { loadBreadcrumbActions } from '@coveo/headless'; + +const breadcrumbActionCreators = loadBreadcrumbActions(headlessEngine); +const action = breadcrumbActionCreators.deselectAllFacets(); + +headlessEngine.dispatch(action); +``` + +**Headless Version 2 Example** + +```ts +import { engine } from './engine'; +import { loadBreadcrumbActions } from '@coveo/headless'; + +const breadcrumbActionCreators = loadBreadcrumbActions(headlessEngine); +const action = breadcrumbActionCreators.deselectAllBreadcrumbs(); + +headlessEngine.dispatch(action); +``` + +### [`DateRangeOptions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.DateRangeOptions.html) + +The `useLocalTime` option was removed. + +In v2, if you don’t want to use the local time, use [`SearchConfigurationOptions.timezone`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchConfigurationOptions.html) instead. +By default, the user local time is used. +To change it, specify the target `timezone` value when initializing your search interface. + +**Headless Version 1 Example** + +```ts +const controller = buildDateFacet(engine, { + options: { + field: 'created', + generateAutomaticRanges: false, + currentValues: [ + buildDateRange({ + start: {period: 'past', unit: 'day', amount: 1}, + end: {period: 'now'}, + useLocalTime: false, + }), + buildDateRange({ + start: {period: 'past', unit: 'week', amount: 1}, + end: {period: 'now'}, + useLocalTime: false, + }), + ], + }, +}); +``` + +**Headless Version 2 Example** + +```ts +import { buildSearchEngine } from '@coveo/headless'; + +export const headlessEngine = buildSearchEngine({ + configuration: { + // ... + search: { + timezone: 'Etc/UTC' + } + } +}); +``` + +### [`SmartSnippetRelatedQuestion`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SmartSnippetRelatedQuestion.html) + +The `documentId` property has been removed. +Use `questionAnswerId` instead. + +In particular, you now need to use `questionAnswerId` rather than `documentId` when using the following methods: + +* [`collapse`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SmartSnippetQuestionsList.html#collapse) +* [`expand`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SmartSnippetQuestionsList.html#expand) + +The same applies when using the following actions: + +* [`collapseSmartSnippetRelatedQuestion`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.QuestionAnsweringActionCreators.html#collapsesmartsnippetrelatedquestion) +* [`expandSmartSnippetRelatedQuestion`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.QuestionAnsweringActionCreators.html#expandsmartsnippetrelatedquestion) +* [`logOpenSmartSnippetSuggestionSource`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ClickAnalyticsActionCreators.html#logopensmartsnippetsuggestionsource) +* [`logExpandSmartSnippetSuggestion`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchAnalyticsActionCreators.html#logexpandsmartsnippetsuggestion) +* [`logCollapseSmartSnippetSuggestion`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchAnalyticsActionCreators.html#logcollapsesmartsnippetsuggestion) + +**Headless Version 1 Example** + +```ts +// ... + +export const SmartSnippetQuestionsList: FunctionComponent< + SmartSnippetQuestionsListProps +> = (props) => { + const {controller} = props; + const [state, setState] = useState(controller.state); + + const {questions} = state; + + // ... + + return ( +
    + People also ask: +
    + {questions.map((question) => { + return ( + <> +
    {question.question}
    +
    + + + +
    + + ); + })} +
    +
    + ); +}; +``` + +**Headless Version 2 Example** + +```ts +// ... + +export const SmartSnippetQuestionsList: FunctionComponent< + SmartSnippetQuestionsListProps +> = (props) => { + const {controller} = props; + const [state, setState] = useState(controller.state); + + const {questions} = state; + + // ... + + return ( +
    + People also ask: +
    + {questions.map((question) => { + return ( + <> +
    {question.question}
    +
    + + + +
    + + ); + })} +
    +
    + ); +}; +``` + +### [`logOpenSmartSnippetSource`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ClickAnalyticsActionCreators.html#logopensmartsnippetsource) + +This action no longer requires the `source` parameter to be specified. +Headless infers it automatically. + +**Headless Version 1** + +```ts +// ... +engine.dispatch(logOpenSmartSnippetSource(result)); +``` + +**Headless Version 2** + +```ts +// ... +engine.dispatch(logOpenSmartSnippetSource()); +``` + +### [`NotifyTrigger`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.NotifyTrigger.html) + +The controller state `notification` property has been replaced by the `notifications` property, which is an array that could contain multiple notifications. + +**Headless Version 1 Example** + +```ts +const notify = () => { + if (state.notification) { + alert('Notification: ' + state.notification); + } +}; +``` + +**Headless Version 2 Example** + +```ts +const notify = () => { + state.notifications.forEach((notification) => { + alert('Notification: ' + notification); + }); +}; +``` + +### [`ExecuteTrigger`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.ExecuteTrigger.html) + +`engine.state.triggers.executed` has been replaced by `engine.state.triggers.executions`, which is an array that could contain multiple executions. +Also, `functionName` and `params` have been removed from `engine.state.triggers` and moved to the executions in the `engine.state.triggers.executions` array. + +**Headless Version 1 Example** + +```ts +// ... + +export class ExecuteTrigger extends Component<{}, ExecuteTriggerState> { + static contextType = AppContext; + context!: ContextType; + + private controller!: HeadlessExecuteTrigger; + private unsubscribe: Unsubscribe = () => {}; + + // ... + + componentDidMount() { + this.controller = buildExecuteTrigger(this.context.engine!); + this.unsubscribe = this.controller.subscribe(() => this.executeFunction()); + } + + private executeFunction = () => { + const {functionName, params} = this.controller.state; + // ... + }; + + // ... +} +``` + +**Headless Version 2 Example** + +```ts +// ... + +export class ExecuteTrigger extends Component<{}, ExecuteTriggerState> { + static contextType = AppContext; + context!: ContextType; + + private controller!: HeadlessExecuteTrigger; + private unsubscribe: Unsubscribe = () => {}; + + // ... + + componentDidMount() { + this.controller = buildExecuteTrigger(this.context.engine!); + this.unsubscribe = this.controller.subscribe(() => + this.controller.state.executions.forEach((execution) => + this.executeFunction(execution) + ) + ); + } + + private executeFunction = (execution: FunctionExecutionTrigger) => { + const {functionName, params} = execution; + // ... + }; + + // ... +} +``` + +### [`querySuggest`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.QuerySuggestActionCreators.html) + +`engine.state.querySuggest.q` has been removed. +Instead, use the more general `engine.state.querySet`, which is also a set of [queries](https://docs.coveo.com/en/231/) (strings) available using the `id` of the target search box (see [`QuerySetActions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.QuerySetActionCreators.html)). + +**Headless Version 1 Example** + +```ts +lastQuery = engine.state.querySuggest.q +// ... +``` + +**Headless Version 2 Example** + +```ts +this.headlessSearchBox = buildSearchBox(headlessEngine, { + options: { + id: '123', + // ... + } + // ... +}) +// ... +lastQuery = engine.state.querySet['123'] +// ... +``` + +### [`facetSet`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.FacetSetActionCreators.html) and [`NumericFacetSet`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.NumericFacetSetActionCreators.html) + +The type of `engine.state.facetSet` has changed from `{ [facetId: string]: FacetRequest }` to `{ [facetId: string]: FacetSlice }`. + +The type of `engine.state.numericFacetSet` has changed from `{ [facetId: string]: NumericFacetRequest }` to `{ [facetId: string]: NumericFacetSlice }`. + +`facetRequest`, which used to be accessible in `facetSet[]`, where `` is the ID of the target facet, is now accessible in `facetSet[].request`. + +**Headless Version 1** + +```ts +lastRequest = engine.state.facetSet[this.headlessFacet.state.facetId] +// ... +``` + +**Headless Version 2** + +```ts +lastRequest = engine.state.facetSet[this.headlessFacet.state.facetId].request +// ... +``` + +`hasBreadcrumbs`, which used to be accessible in `facetRequest`, is now accessible in `FacetSlice`. +Since the type of `engine.state.facetSet` has changed accordingly, the behavior should remain unchanged. +In other words, the following are equivalent: + +**Headless Version 1** + +```ts +engine.state.facetSet[].hasBreadcrumbs +``` + +**Headless Version 2** + +```ts +engine.state.facetSet[].hasBreadcrumbs +``` + +### Internal controllers removal + +The following internal controllers have been removed: + +* `InteractiveResultCore` +* `CoreQuerySummary` +* `CoreResultList` +* `CoreFacetManager` +* `CoreStatus` +* `DocumentSuggestion` (from the Case Assist engine) +* `QuickviewCore` + +### `Redirection` + +The `engine.state.redirection` reducer (not documented) and the related actions (not documented either) have been removed. +If you were using them previously, see [`StandaloneSearchBox`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.StandaloneSearchBox.html) and [`StandaloneSearchBoxSetActions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Commerce.StandaloneSearchBoxSetActionCreators.html) instead. + +### [`FacetOptions`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.FacetOptions.html) + +The `delimitingCharacter` option was removed. +It wasn’t doing anything, since delimiting characters are only relevant in category [facets](https://docs.coveo.com/en/198/). + +### [Result template manager](https://docs.coveo.com/en/headless/latest/usage/result-templates/) + +Registering an invalid template now throws an error rather than just logging it. +Previously, the invalid template wouldn’t work, but it wouldn’t throw an error. +So, this change will only throw an error if you already had a result template issue. + +The error could look like the following examples: + +```text +Each result template conditions should be a function that takes a result as an argument and returns a boolean +``` + +```text +The following properties are invalid: + content: value is required. + conditions: value is required. +``` + +### `clientOrigin` + +The `clientOrigin` has been changed for some Search API requests (see [Modify requests and responses](https://docs.coveo.com/en/headless/latest/usage/headless-modify-requests-responses/)). +This change should be transparent, unless you were modifying the `clientOrigin` yourself. + +| Request | +| --- | +| `clientOrigin` value | +| Product listing | +| `commerceApiFetch` | +| Case assist | +| `caseAssistApiFetch` | +| Insight | +| `insightApiFetch` | + +### Search analytics actions + +All of the [`log*` search action creators](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchAnalyticsActionCreators.html) now return dispatchable actions of type `PreparableAnalyticsAction` rather than `AsyncThunkAction<{ analyticsType: AnalyticsType.Search; }, void, AsyncThunkAnalyticsOptions>`. +[Search actions](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.SearchActionCreators.html) now require this new type of dispatchable analytics actions to trigger. + +The change should be transparent, because only the types used are different, not the use of the functions involved themselves. \ No newline at end of file diff --git a/packages/headless/source_docs/upgrade-from-v2-to-v3.md b/packages/headless/source_docs/upgrade-from-v2-to-v3.md new file mode 100644 index 00000000000..1b976fbf6a9 --- /dev/null +++ b/packages/headless/source_docs/upgrade-from-v2-to-v3.md @@ -0,0 +1,509 @@ +--- +title: v2 to v3 +group: Upgrade +slug: upgrade/v2-to-v3 +--- +# Upgrade from v2 to v3 +[Headless](https://docs.coveo.com/en/lcdf0493/) v3 introduces changes and innovations that align with the latest evolution of the [Coveo Platform](https://docs.coveo.com/en/186/). + +
    ❗ IMPORTANT: The following are breaking changes from Headless v2 to v3
    + +
    + +## Migration to Coveo Event Protocol + +Coveo [Event Protocol](https://docs.coveo.com/en/headless/latest/usage/headless-usage-analytics/headless-ep/) becomes the new default protocol. +In other words, the default value of `configuration.analytics.analyticsMode` in the Engine is now `next` instead of `legacy`. + +If you’re using the default value in v3 and aren’t yet ready to [migrate to the new protocol](https://docs.coveo.com/en/o88d0509/), set the value to `legacy` to continue using the Coveo UA protocol. + +
    ❗ IMPORTANT
    + +Only [Coveo for Commerce](https://docs.coveo.com/en/1499/) supports EP at the moment. +For all other implementations, set `analyticsMode` to `legacy` when upgrading to v3. +
    + +```ts +const engine = buildSearchEngine({ + configuration: { + // ...rest of configuration + analytics: {analyticsMode: 'legacy'}, + } +}) +``` + +### Removal of `analyticsClientMiddleware` function + +As part of the migration to support Coveo Event Protocol (EP), [Headless](https://docs.coveo.com/en/lcdf0493/) won’t support the `analyticsClientMiddleware` property when EP is enabled. + +**Headless Version 2** + +```ts +const engine = buildSearchEngine({ + configuration: { + // ...rest of configuration + analytics: { + analyticsClientMiddleware: (eventName: string, payload: Record) => { + // ... + } + } + } +}) +``` + +There’s no alternative when using EP, as EP is meant to be more streamlined, which results in cleaner data and more powerful [machine learning](https://docs.coveo.com/en/188/) [models](https://docs.coveo.com/en/1012/). +When using the legacy Coveo UA protocol, you can continue using `analyticsClientMiddleware`. + +**Headless Version 3** + +```ts +const engine = buildSearchEngine({ + configuration: { + // ...rest of configuration + analytics: { + analyticsMode: 'legacy', + analyticsClientMiddleware: (eventName: string, payload: any) => { + // ... + } + } + } +}) +``` + +## Organization endpoints + +[Organization endpoints](https://docs.coveo.com/en/mcc80216/) is a feature that improves separation of concerns and resiliency, making multi-region and data residency deployments smoother. + +Starting with Headless v3, the usage of organization endpoints will be enforced automatically, as opposed to optional in v2. + +**Headless Version 2** + +```ts +import {buildSearchEngine} from '@coveo/headless'; + +const engine = buildSearchEngine({ + configuration: { + // ... + organizationId: '', + organizationEndpoints: getOrganizationEndpoints('') + } +}) +``` + +**Headless Version 3** + +```js +import {buildSearchEngine} from '@coveo/headless'; + +const engine = buildSearchEngine({ + configuration: { + // ... + organizationId: '', + } +}) +``` + +For [HIPAA](https://docs.coveo.com/en/1853/) organizations, rather than specifying the `hipaa` argument in the `getOrganizationEndpoints` function, set the `environment` property to `hipaa` in your engine configuration. + +**Headless Version 2** + +```ts +import {buildSearchEngine} from '@coveo/headless'; + +const engine = buildSearchEngine({ + configuration: { + // ... + organizationId: '', + organizationEndpoints: getOrganizationEndpoints('', 'hipaa') + } +}) +``` + +**Headless Version 3** + +```js +import {buildSearchEngine} from '@coveo/headless'; + +const engine = buildSearchEngine({ + configuration: { + // ... + organizationId: '', + environment: 'hipaa', + } +}) +``` + +For most implementations, this is the extent of the changes. +However, if you used the `organizationEndpoints` property to send requests to your own proxy that relays requests to Coveo APIs, more changes are required. +Headless v3 introduces the `search.proxyBaseUrl`, `analytics.proxyBaseUrl`, and `commerce.proxyBaseUrl` engine configuration options for such cases. + +**Headless Version 2** + +```ts +import {buildSearchEngine} from '@coveo/headless'; + +const engine = buildSearchEngine({ + configuration: { + // ... + organizationId: 'my-org-id', + organizationEndpoints: + { + ...getOrganizationEndpoints('my-org-id'), + search: 'https://myproxy.com/search', + } + } +}) +``` + +**Headless Version 3** + +```js +import {buildSearchEngine} from '@coveo/headless'; + +const engine = buildSearchEngine({ + configuration: { + // ... + organizationId: 'my-org-id', + search: { + proxyBaseUrl: 'https://myproxy.com/search', + }, + } +}) +``` + +If you were using the `getOrganizationEndpoints` function for some other purpose, you can use the new `getOrganizationEndpoint`, `getAdministrationOrganizationEndpoint`, `getSearchApiBaseUrl` or `getAnalyticsNextApiBaseUrl` functions instead. + +**Headless Version 2** + +```ts +const organizationEndpoints: getOrganizationEndpoints(''); +const searchEndpoint = organizationEndpoints.search; +// ... +``` + +**Headless Version 3** + +```js +const searchEndpoint = getSearchApiBaseUrl(''); +// ... +``` + +### `platformUrl` property + +The `platformUrl` property, which was previously deprecated, has been removed from all engine configuration options. +This property was originally used to specify [Organization endpoints](https://docs.coveo.com/en/mcc80216/), but the `organizationEndpoints` property has since replaced it. +As organization endpoints are now mandatory, the `platformUrl` property is no longer needed. + +## Node version support + +The minimum [version of Node.js](https://nodejs.org/en/about/previous-releases) supported by Headless is now 20. + +This is strictly for clients bundling and building their front-end application through a Node based toolchain and including Headless as a project dependency. + +## Exports + +Headless exports multiple entry points each meant to support different use cases and environments. +To improve the inter-operability of Headless with different industry standard tools, and to guide those tools to properly use the appropriate entry points depending on the use case, Headless will start enforcing and specifying [export fields in package.json](https://nodejs.org/api/packages.html#modules-packages). +This means that implementation relying on non-public modules of the package will stop functioning. + +**Headless Version 3** + +```js +// This will be blocked entirely +import {nonPubliclyDocumentedFunction} from '@coveo/headless/dist/nonPublicFile.js'; + +// Will throw an error, or won't compile +nonPubliclyDocumentedFunction(); +``` + +Also, it means you need to set `moduleResolution": "bundler"` in your `tsconfig.json` file to access secondary entry points such as `@coveo/headless/commerce` or `@coveo/headless/ssr`. +See [TypeScript module resolution](https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution) for more information. + +### `moduleResolution` in `tsconfig.json` when installing via [npm](https://www.npmjs.com/) + +If you use Typescript, note that the `node10`/`node` module resolution is no longer supported. +The `classic` module resolution, which was never supported, remains unsupported. + +See [TypeScript module resolution](https://www.typescriptlang.org/docs/handbook/modules/theory.html#module-resolution) and [Announcing TypeScript 5.0 `--moduleResolution bundler`](https://devblogs.microsoft.com/typescript/announcing-typescript-5-0/#--moduleresolution-bundler). + +### `abab` dependency + +The deprecated [`abab`](https://www.npmjs.com/package/abab) npm package has been removed from Headless dependencies. +If you were using it through Headless, you’ll need to include it in your project dependencies directly. + +### `TestUtils` + +In v2, some internal `TestUtils` functions were exported, but weren’t meant for public use. +Those functions are no longer exported in v3. + +### `createAction`, `createAsyncThunk` and `createReducer` + +Those [Redux Toolkit](https://redux-toolkit.js.org/) functions are no longer re-exported by Headless. +If you’re using them, import then directly from Redux. + +## Modified behaviors + +### DidYouMean `queryCorrectionMode` + +The [`queryCorrectionMode`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.DidYouMeanOptions.html) default value is now `next` rather than `legacy`. +This means that the default behavior of the `didYouMean` controller will be to use a [query suggestions](https://docs.coveo.com/en/1015/) [model](https://docs.coveo.com/en/1012/) for query correction rather than the legacy [index](https://docs.coveo.com/en/204/) mechanism. + +### GeneratedAnswer [`sendFeedback`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.GeneratedAnswer.html#sendfeedback) + +The `feedback` parameter of the `sendFeedback` method was changed. +In v2, it could have either the `GeneratedAnswerFeedback` or `GeneratedAnswerFeedbackV2` type, which were defined as follows: + +**Headless Version 2** + +```ts +export type GeneratedAnswerFeedback = + | 'irrelevant' + | 'notAccurate' + | 'outOfDate' + | 'harmful'; + +export type GeneratedAnswerFeedbackOption = 'yes' | 'unknown' | 'no'; + +export type GeneratedAnswerFeedbackV2 = { + helpful: boolean; + documented: GeneratedAnswerFeedbackOption; + correctTopic: GeneratedAnswerFeedbackOption; + hallucinationFree: GeneratedAnswerFeedbackOption; + readable: GeneratedAnswerFeedbackOption; + details?: string; + documentUrl?: string; +}; +``` + +In v3, only the `GeneratedAnswerFeedback` type is accepted, which was the previous `GeneratedAnswerFeedback2`. + +**Headless Version 3** + +```ts +export type GeneratedAnswerFeedbackOption = 'yes' | 'unknown' | 'no'; + +export type GeneratedAnswerFeedback = { + helpful: boolean; + documented: GeneratedAnswerFeedbackOption; + correctTopic: GeneratedAnswerFeedbackOption; + hallucinationFree: GeneratedAnswerFeedbackOption; + readable: GeneratedAnswerFeedbackOption; + details?: string; + documentUrl?: string; +}; +``` + +You must therefore adjust your code to use the new `GeneratedAnswerFeedback` type. + +**Headless Version 2** + +```ts +const feedback: GeneratedAnswerFeedback = 'irrelevant'; +generatedAnswer.sendFeedback(feedback); +``` + +**Headless Version 3** + +```js +const feedback: GeneratedAnswerFeedback = { + readable: 'unknown', + correctTopic: 'unknown', + documented: 'yes', + hallucinationFree: 'no', + helpful: false, +}; +generatedAnswer.sendFeedback(feedback); +``` + +The undocumented `logGeneratedAnswerFeedback` action was also modified the same way, accepting only `GeneratedAnswerFeedbackV2` input, which was renamed to `GeneratedAnswerFeedback`. + +The undocumented `logGeneratedAnswerDetailedFeedback` action was removed in v3. + +### Relevance Generative Answering (RGA) citation clicks now tracked as regular click events + +As of the Headless `v3.3.0` release, Coveo [Relevance Generative Answering (RGA)](https://docs.coveo.com/en/nbtb6010/) citation clicks are now tracked as regular click events instead of custom click events in [Coveo Analytics](https://docs.coveo.com/en/182/) reports. +As a result, citation click events now have a [click rank](https://docs.coveo.com/en/2948#click-rank) value of `1`. +Additionally, the click **Event Cause** value is set to [`generatedAnswerCitationClick`](https://docs.coveo.com/en/2948#generatedanswercitationclick). + +This change applies regardless of the tracking protocol (Coveo UA or Coveo [Event Protocol](https://docs.coveo.com/en/o9je0592/)) used. + +### Types + +The undocumented `SearchBoxSuggestionsEvent` type now takes in a search box controller and bindings as input. + +**Headless Version 2** + +```ts +const event: SearchBoxSuggestionsEvent, +``` + +**Headless Version 3** + +```ts +const event: SearchBoxSuggestionsEvent, +``` + +## Removals + +### Relevance Generative Answering (RGA) `rephrase` option + +Many actions, properties, types and methods related to the Relevance Generative Answering `rephrase` option were removed or modified in v3, since the option itself is no longer supported. + +* All [`rephrase`](https://docs.coveo.com/en/headless/2.80.7/reference/search/controllers/generated-answer#rephrase-method) methods were removed from all `*-generated-answer` controllers. +* The undocumented `logRephraseGeneratedAnswer` analytics action was removed. +* The undocumented `rephraseGeneratedAnswer` search action cause was removed. +* The `GeneratedResponseFormat.answerStyle` was removed from the [GeneratedAnswer `state`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.GeneratedAnswer.html#state). +* The `GeneratedAnswerStyle` type was removed. +* The undocumented `updateResponseFormat` action now only accepts the `contentFormat` option, and not the answer style. + +### Sub-packages + +Both `product-recommendations` and `product-listing` sub-packages are removed in v3. +Use the new `commerce` package instead. +See [Headless for commerce](https://docs.coveo.com/en/o52e9091/). + +### Properties and methods + +The following were removed in v3: + +* [`buildCaseAssistQuickview`](https://docs.coveo.com/en/headless/2.80.7/reference/case-assist/controllers/case-assist-quickview#buildcaseassistquickview), which was a duplicate export of [`buildQuickview`](https://docs.coveo.com/en/headless/latest/reference/functions/Search.buildQuickview.html) has been removed. +* [`buildCaseAssistInteractiveResult`](https://docs.coveo.com/en/headless/2.80.7/reference/case-assist/controllers/case-assist-interactive-result#buildcaseassistinteractiveresult), which was a duplicate export of [`buildInteractiveResult`](https://docs.coveo.com/en/headless/latest/reference/functions/Search.buildInteractiveResult.html) has been removed. +* `browserPostLogHook`, which used to be exposed in the engine configuration options, has been removed. +It wasn’t doing anything. +* The quickview `onlyContentURL` initialization option has been removed from [`Quickview`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.QuickviewOptions.html) and [`CaseAssistQuickview`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Case_Assist.QuickviewOptions.html) controllers because it was always set to `true`. + + Similarly, the `content` state attribute has been removed from those controllers, as it was always empty. +* The undocumented `showMoreSmartSnippetSuggestion` and `showLessSmartSnippetSuggestion` search page events have been removed. +* [CategoryFacet `state`](https://docs.coveo.com/en/headless/latest/reference/interfaces/Search.CategoryFacet.html#state) properties `parents` and `values` have been removed. +Use `valuesAsTrees` and `selectedValueAncestry` instead. + + While `values` was a flat list of all values, `valuesAsTrees` contains the root facet values, whose children, if any, are accessible via `valuesAsTrees[i].children[j]`. + + `selectedValueAncestry` is similar to `parents`, but it also includes the selected value itself. + + Depending on your implementation, the changes may or may not be transparent. + + **Headless Version 2** + + ```tsx + private renderParents() { + return ( + this.state.hasActiveValues && ( +
    + Filtering by: {this.renderClearButton()} + {this.state.parents.map((parentValue, i) => { + const isSelectedValue = i === this.state.parents.length - 1; + + return ( + + → + {!isSelectedValue ? ( + + ) : ( + {parentValue.value} + )} + + ); + })} +
    + ) + ); + } + + private renderActiveValues() { + return ( +
      + {this.state.values.map((value) => ( +
    • + +
    • + ))} +
    + ); + } + ``` + + **Headless Version 3** + + ```tsx + private renderParents() { + return ( + this.state.hasActiveValues && ( +
    + Filtering by: {this.renderClearButton()} + {this.state.valuesAsTrees.map((parentValue, i) => { + const isSelectedValue = i === this.state.valuesAsTrees.length - 1; + + return ( + + → + {!isSelectedValue ? ( + + ) : ( + {parentValue.value} + )} + + ); + })} +
    + ) + ); + } + + private renderActiveValues() { + return ( +
      + {this.state.selectedValueAncestry.map((value) => ( +
    • + +
    • + ))} +
    + ); + } + ``` + +### Actions + +#### [`logExpandToFullUI` parameters](https://docs.coveo.com/en/headless/latest/reference/interfaces/Insight.InsightSearchAnalyticsActionCreators.html#logexpandtofullui) + +The `LogExpandToFullUI` action no longer uses the `caseId` and `caseNumber` parameters. +They are rather fetched automatically from the Headless engine state. + +The `fullSearchComponentName` and `triggeredBy` parameters have been removed. +They had no effect. + +#### `InsightCaseContextSection` actions + +These undocumented actions have been removed. + +### Types + +The undocumented `CommerceSearchBoxSuggestionsEvent` type has been removed in V3. +If you were using this type, switch to the undocumented `SearchBoxSuggestionsEvent` type, which now accepts a search box controller and bindings as input. + +**Headless Version 2** + +```ts +const event: SearchBoxSuggestionsEvent, +``` + +**Headless Version 3** + +```ts +const event: SearchBoxSuggestionsEvent, +-- \ No newline at end of file diff --git a/packages/headless/source_docs/versions.md b/packages/headless/source_docs/versions.md new file mode 100644 index 00000000000..0d8ce38f9ae --- /dev/null +++ b/packages/headless/source_docs/versions.md @@ -0,0 +1,26 @@ +--- +title: Versioned documentation +slug: versioned-documentation +--- +# Versioned documentation +
    💡 TIP
    + +For detailed changes between versions, see the [Change Log](https://github.com/coveo/ui-kit/blob/master/packages/headless/CHANGELOG.md). +
    + +
    📌 Note
    + +As of v1.28.2 the default analytics endpoint has moved to `https://analytics.cloud.coveo.com/rest/ua` +
    + +## Latest version (v3.35.0) + +[Documentation](https://docs.coveo.com/en/headless/latest/) + +## 2.80.7 + +[Documentation](https://docs.coveo.com/en/headless/2.80.7/) + +## 1.114.0 + +[Documentation](https://docs.coveo.com/en/headless/1.114.0/) \ No newline at end of file diff --git a/packages/headless/typedoc-configs/case-assist.typedoc.json b/packages/headless/typedoc-configs/case-assist.typedoc.json index ba96ff66953..36cd724dc93 100644 --- a/packages/headless/typedoc-configs/case-assist.typedoc.json +++ b/packages/headless/typedoc-configs/case-assist.typedoc.json @@ -5,14 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], + "groupOrder": ["*"], "name": "Case Assist", "readme": "none", "entryPoints": ["../src/case-assist.index.ts"], diff --git a/packages/headless/typedoc-configs/commerce.typedoc.json b/packages/headless/typedoc-configs/commerce.typedoc.json index 83c3773684e..c41bfd84989 100644 --- a/packages/headless/typedoc-configs/commerce.typedoc.json +++ b/packages/headless/typedoc-configs/commerce.typedoc.json @@ -5,14 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], + "groupOrder": ["*"], "name": "Commerce", "readme": "none", "entryPoints": ["../src/commerce.index.ts"], diff --git a/packages/headless/typedoc-configs/index.typedoc.json b/packages/headless/typedoc-configs/index.typedoc.json index 152d4e91df5..6d55a105fd6 100644 --- a/packages/headless/typedoc-configs/index.typedoc.json +++ b/packages/headless/typedoc-configs/index.typedoc.json @@ -5,14 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], + "groupOrder": ["*"], "name": "Search", "readme": "none", "entryPoints": ["../src/index.ts"], diff --git a/packages/headless/typedoc-configs/insight.typedoc.json b/packages/headless/typedoc-configs/insight.typedoc.json index be34e136fe6..f153cf9561e 100644 --- a/packages/headless/typedoc-configs/insight.typedoc.json +++ b/packages/headless/typedoc-configs/insight.typedoc.json @@ -5,14 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], + "groupOrder": ["*"], "name": "Insight", "readme": "none", "entryPoints": ["../src/insight.index.ts"], diff --git a/packages/headless/typedoc-configs/recommendation.typedoc.json b/packages/headless/typedoc-configs/recommendation.typedoc.json index ef67acf22a9..f3b73e9d558 100644 --- a/packages/headless/typedoc-configs/recommendation.typedoc.json +++ b/packages/headless/typedoc-configs/recommendation.typedoc.json @@ -5,14 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], + "groupOrder": ["*"], "name": "Recommendation", "readme": "none", "entryPoints": ["../src/recommendation.index.ts"], diff --git a/packages/headless/typedoc-configs/ssr-commerce.typedoc.json b/packages/headless/typedoc-configs/ssr-commerce.typedoc.json index d8801132a71..38be6cb9304 100644 --- a/packages/headless/typedoc-configs/ssr-commerce.typedoc.json +++ b/packages/headless/typedoc-configs/ssr-commerce.typedoc.json @@ -5,15 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Definers", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], + "groupOrder": ["*"], "name": "SSR Commerce", "readme": "none", "entryPoints": ["../src/ssr-commerce.index.ts"], diff --git a/packages/headless/typedoc-configs/ssr.typedoc.json b/packages/headless/typedoc-configs/ssr.typedoc.json index 73e3e6390be..bbd4c18702c 100644 --- a/packages/headless/typedoc-configs/ssr.typedoc.json +++ b/packages/headless/typedoc-configs/ssr.typedoc.json @@ -5,15 +5,7 @@ "includeFolders": true }, "categorizeByGroup": true, - "groupOrder": [ - "Engine", - "Definers", - "Controllers", - "Buildable controllers", - "Sub-controllers", - "Actions", - "*" - ], + "groupOrder": ["*"], "name": "SSR Search", "readme": "none", "entryPoints": ["../src/ssr.index.ts"], diff --git a/packages/headless/typedoc.json b/packages/headless/typedoc.json index 8fea97696f6..313ba389e6d 100644 --- a/packages/headless/typedoc.json +++ b/packages/headless/typedoc.json @@ -1,10 +1,49 @@ { + "highlightLanguages": [ + "typescript", + "javascript", + "jsx", + "tsx", + "json", + "bash", + "html" + ], + "projectDocuments": [ + "source_docs/coveo-headless-home.md", + "source_docs/headless-code-samples.md", + "source_docs/coveo-headless-usage.md", + "source_docs/dependent-facets.md", + "source_docs/extend-headless-controllers.md", + "source_docs/headless-modify-requests-responses.md", + "source_docs/headless-proxy.md", + "source_docs/headless-saml-authentication.md", + "source_docs/headless-usage-analytics-coveo-ua.md", + "source_docs/headless-usage-analytics-ep.md", + "source_docs/headless-usage-analytics.md", + "source_docs/headless-view-events-ep.md", + "source_docs/headless-view-events.md", + "source_docs/highlighting.md", + "source_docs/product-lifecycle.md", + "source_docs/result-templates.md", + "source_docs/server-side-rendering.md", + "source_docs/ssr-extend-engine-definitons.md", + "source_docs/ssr-implement-search-parameter-support.md", + "source_docs/ssr-send-context.md", + "source_docs/ssr-usage.md", + "source_docs/standalone-search-box.md", + "source_docs/synchronize-search-parameters-with-the-url.md", + "source_docs/upgrade-from-v1-to-v2.md", + "source_docs/upgrade-from-v2-to-v3.md", + "source_docs/versions.md", + "README.md" + ], "entryPointStrategy": "merge", "navigation": { "includeCategories": true, "includeGroups": true, - "includeFolders": true + "includeFolders": false }, + "readme": "source_docs/coveo-headless-home.md", "categorizeByGroup": true, "entryPoints": [ "./temp/index.json", @@ -15,5 +54,78 @@ "./temp/insight.json", "./temp/recommendation.json" ], - "plugin": ["@coveo/documentation"] + "plugin": ["@coveo/documentation"], + "hoistOther.topLevelOrder": [ + "Home", + "Usage", + "Reference", + "Code samples", + "Product lifecycle", + "Upgrade", + "Versioned documentation" + ], + "hoistOther.nestedOrder": { + "usage server-side rendering": ["Introduction", "*"], + "upgrade": ["Introduction", "*"], + "usage usage analytics": ["Introduction", "*"], + "usage": ["Introduction", "*"], + "reference search": [ + "Engine", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ], + "reference case assist": [ + "Engine", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ], + "reference commerce": [ + "Engine", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ], + "reference insight": [ + "Engine", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ], + "reference recommendation": [ + "Engine", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ], + "reference ssr commerce": [ + "Engine", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ], + "reference ssr search": [ + "Engine", + "Controllers", + "Buildable controllers", + "Sub-controllers", + "Actions", + "*" + ] + }, + "hoistOther.renameModulesTo": "Reference", + "router": "kebab" }