It's been a solid 6 months since we posted here about Coveo's Headless product. And wow has it matured in that timeframe. It's now at version 1.41.7 and in my recent refresher oh so smooth it was getting it all set up. I vaguely remember having some challenges initially. And let me tell you, none of what I encountered was present this time around.
So let's have a look, shall we.
So before we dive right in and install @coveo/headless, let's do some pre-work. Something that I think would have helped us a bit more the first time around.
Think of NVM as a version manager for Node. In the past, you might have only had a single version of Node on your machine at any given time. With NVM you can have multiple versions loaded and select which one you need for whatever project you are working on. You can download NVM for Mac / Linux or download for Windows.
Given Headless requires a Node.js version of 12 or greater, you can first check which version you're running and if need be, using NVM, load a newer version.
Yes, you will want to have Git as, a) it's just good practice and b) it could save you time in the end when building components that you can simply and easily branch off and not have it interfere with your core application. Get Git here.
Headless is written in TypeScript, as such, when we create a project for Headless, it's just recommended to use TypeScript. Install it on your system by loading up a Command Prompt and typing:
npm install -g typescript
One thing I've learned in my short time of building with Headless is that this next step is pretty darn important. So let's set one up right now. Find a location on your computer that you'd like to run the project from, and once you have a name in mind (e.g. headless-project), type the following:
npx create-react-app --template typescript headless-project
Yes, for the purposes of this article, we are using React. You could use Vue or Angular if so desired. The important part, is the --template typescript
portion as this will ensure the project is properly supporting TypeScript.
You've made it this far and now is the time, let's install @covoe/headless. Move into your project, cd headless-project
and then type the following:
npm install @coveo/headless
Let's have a look at what our project looks like right now.
Two important folder structures to be aware of:
/build
- This is what gets updated upon running npm run build
. It is what you will effectively be browsing when you run npm start
/src
- Where the magic is constructed. Right now, it's pretty bare-bones, but we will flesh it out here in a few moments.Now we're going to run through some core concepts involved when building a Headless project.
This is the piece that basically gives our project juice. As it exists, it holds the entire Headless state for the search interface.
As of version 1.41.7, there are four different types of engines within Headless. They are as follows:
For the purposes of this article, we're going to focus on the Search Engine but in the future, we will look to cover others.
Let's build a basic engine, shall we? Create a new file and call it Engine.tsx
in the root. Fill it with the following (updating with your own organizationId
and accessToken
from the Coveo Platform).
import {
buildSearchEngine,
} from '@coveo/headless';
export const headlessEngine = buildSearchEngine({
configuration: {
accessToken:'xxxxxxxxx-x-xxxx-xxxx-xxxxxxxxx',
organizationId: 'zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz'
}});
That's it, for now at least. With this bit, we can now proceed with the initial setup of the App.tsx file to manage the search interface and execute searches.
Within the App.tsx
at the top of the file, your import statements will need to be updated to reflect the following:
import React from 'react';
import {useEffect} from 'react'; // Needed to run the engine
//Coveo Headless
import {loadSearchAnalyticsActions, loadSearchActions} from '@coveo/headless';
import {headlessEngine} from './Engine';
Then within the App()
function, we're going to load in the engine.
function App() {
useEffect(() => {
const {logInterfaceLoad} = loadSearchAnalyticsActions(headlessEngine);
const {executeSearch} = loadSearchActions(headlessEngine);
headlessEngine.dispatch(executeSearch(logInterfaceLoad()));
});
return (
<div className="App">
<header className="App-header">
<h1>Coveo Headless Search Interface</h1>
</header>
<main>
</main>
</div>
);
}
When it comes to Headless components, they are organized with a corresponding component and its accompanying controller. The controller is the primary way to interact with the engine's state.
It's best to learn by example, so let's set this up.
First, let's create our component. Create a directory called components
. Inside that folder, create a file called search-box.tsx
. The code you will be inserting, which is a good starting point, is as follows. The original source code, and potentially improved code, can be found here.
import {SearchBox as HeadlessSearchBox} from '@coveo/headless';
import {FunctionComponent, useEffect, useState} from 'react';
interface SearchBoxProps {
controller: HeadlessSearchBox;
}
export const SearchBox: FunctionComponent<SearchBoxProps> = (props) => {
const {controller} = props;
const [state, setState] = useState(controller.state);
const [focused, setFocused] = useState(false);
useEffect(() => controller.subscribe(() => setState(controller.state)), [
controller,
]);
// style used within the search box
const suggestionStyle = {
cursor: 'pointer',
};
// What is returned when the component is called
return (
<div className="search-box">
<input
value={state.value}
onChange={(e) => controller.updateText(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
controller.submit();
} else if (e.key === 'Escape') {
controller.clear();
(e.target as HTMLInputElement).blur();
}
}}
onFocus={() => setFocused(true)}
onBlur={() => setFocused(false)}
/>
<button onClick={() => controller.submit()}>Search</button>
<button onClick={() => controller.clear()}>Clear</button>
{focused && state.suggestions.length > 0 && (
<ul>
{state.suggestions.map((suggestion) => {
return (
<li
style={suggestionStyle}
key={suggestion.rawValue}
onMouseDown={(e) => e.preventDefault()}
onClick={() => controller.selectSuggestion(suggestion.rawValue)}
dangerouslySetInnerHTML={{__html: suggestion.highlightedValue}}
></li>
);
})}
</ul>
)}
</div>
);
};
The nice thing is, when it comes to controllers, think of these as the options that can be configured within the component. In the case of the Search Box. You have options such as highlighting.
import {
buildSearchBox,
} from '@coveo/headless';
import {headlessEngine} from '../engine';
export const searchBox = buildSearchBox(headlessEngine, {
options: {
highlightOptions: {
notMatchDelimiters: {
open: '<strong>',
close: '</strong>',
},
correctionDelimiters: {
open: '<i>',
close: '</i>',
},
},
},
});
Once you have the component (and if needed, the controller) in place it's time to add it to the App. So open up your App.tsx
file and start by adding the necessary import
statements.
import {SearchBox} from './components/search-box';
import {
searchBox,
} from './controllers/controllers';
With that in place, let's add the component to the app body.
<div className="App">
<header className="App-header">
<h1>Coveo Headless Search Interface</h1>
</header>
<main className="App-body">
<div className="search-section">
<SearchBox controller={searchBox} />
</div>
</main>
</div>
Once saved, run the following to compile the TypeScript and run the app.
npm run build
npm run start
Voila! This is just the start. Obviously not much happens with the Search Box so, in our next article, we will look into the Result List.
Sign up to our bi-weekly newsletter for a bite-sized curation of valuable insight from the Sitecore community.