Creating Components in Coveo Headless
Coveo has built an awesome library that helps create UI components with more control. The library contains separated parts of the search page as different controllers, allowing you to choose only the parts you need instead of hiding them with CSS styles. They can also work synchronize separately, giving you full control of how your components behave based on certain conditions.
What are Controllers in Coveo Headless?
In Coveo Headless, they have built headless controllers to provide methods that trigger different high-level behaviors. These are basically the actions provided to us for our UI search components in order to connect to Coveo. These can range from any of the following actions:
- update the query
- update the URL based on updates to facets
- move to a different page
- provide a list of results
Basically any function or action needed to work for a search page has been made to interact with Coveo using their controllers.
Coveo Headless Basic Setup
Let’s start off by having the basic setup to have Coveo Headless working, this needs to be setup right because if one function runs earlier than the rest might mess up synchronizing data within the different controllers.
Engine.tsx
The engine is the source of all the moving parts of building the headless coveo functionality. The controllers will be using the same engine data to be in sync.
import { buildSearchEngine, SearchEngine } from '@coveo/headless';
export function initializeHeadlessEngine(): SearchEngine {
return buildSearchEngine({
configuration: {
platformUrl: 'https://platform.cloud.coveo.com',
organizationId: `coveoorgnid`, // replace with the orgID provided by Coveo
accessToken: `XXX-XXXXX-XXXX`, // replace with the accessToken provided by Coveo
},
});
}
SearchPage.tsx
import { initializeHeadlessEngine} from './Engine';
import {
loadSearchActions,
loadSearchAnalyticsActions,
loadSearchHubActions,
SearchEngine,
} from '@coveo/headless';
import {
urlManagerController,
} from 'lib/controllers/controllers';
const SearchPage = (): JSX.Element => {
const [engine, setEngine] = React.useState<SearchEngine | null>(null);
useEffect(() => {
setEngine(initializeHeadlessEngine(fields));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (engine) {
const { setSearchHub } = loadSearchHubActions(engine);
const { logInterfaceLoad } = loadSearchAnalyticsActions(engine);
const { executeSearch } = loadSearchActions(engine);
urlManagerController(engine, window.location.hash.slice(1));
engine.dispatch(executeSearch(logInterfaceLoad()));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine]);
if (engine) {
return (
<div>
{/* Components will be here */}
</div>
);
} else {
return <></>;
}
};
export default SearchPage;
This piece of code may look overwhelming but trust the process. I’ll walk you through the parts of the code and give some idea to what they are used for.
const [engine, setEngine] = React.useState<SearchEngine | null>(null);
useEffect(() => {
setEngine(initializeHeadlessEnginev2(fields));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
One important thing to look out for is the chances of multiple engine
are being used. This will mess up synchronizing without you knowing. Make sure that once the engine has been initialized you aren’t creating multiple engines afterwards.
useEffect(() => {
if (engine) {
const { logInterfaceLoad } = loadSearchAnalyticsActions(engine);
const { executeSearch } = loadSearchActions(engine);
urlManagerController(engine, window.location.hash.slice(1));
engine.dispatch(executeSearch(logInterfaceLoad()));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [engine]);
After the engine has been initialized, this will be triggered and only triggered once the engine has been set. logInterfaceLoad
is required in order for the initial search to work. This is also a useful tool to have Coveo Analytics working on your account. There are actually more analytics functions to grab from loadSearchAnalyticsActions
depending on what you are required to observe. You can check the documentation out.
const { logInterfaceLoad } = loadSearchAnalyticsActions(engine);
Coveo Headless has also created other actions for when doing a search. There are different search actions you can use based on the documentation in Coveo.
const { executeSearch } = loadSearchActions(engine);
This is where you will meet your first Controller. This isn’t really required, well none of the controllers are required. It’ll depend on what you are after, the URLManager
is basically in charge of giving URL fragments that will used to add to the current URL. This can also be used to trigger the search page to show a specific state of search based on how the URL looks like.
urlManagerController(engine, window.location.hash.slice(1));
Just my personal preference, I have placed all of the different controllers in one file just so I can keep track of what controllers I can use.
controllers.ts
import { SearchEngine, UrlManager, buildUrlManager } from '@coveo/headless';
export const urlManagerController = (engine: SearchEngine, fragment: string): UrlManager => {
return buildUrlManager(engine, {
initialState: { fragment },
});
};
This is used to create your controller, also used to get the initial state based on the fragment provided. Some helpful tips as well, you don’t need to create just one controller, you may create multiple controllers when needed.
engine.dispatch(executeSearch(logInterfaceLoad()));
Finally, this line is used to trigger the initial Search, we placed the initialization of the URLManagerController first because we want our initial search to also be the results based on the structure of the URL.
With the code, you won’t get to see anything actually, we’ll need more components and controllers in order to get more.
Rendering a List of Results in Coveo
Let me introduce another controller you will probably have on most search pages, the ResultList. First off let’s add another controller in our controllers.tsx
, you can checkout what the buildResultList
needs on the Coveo documentation.
import {
SearchEngine, UrlManager, buildUrlManager, ResultList, buildResultList } from '@coveo/headless';
export const resultController = (engine: SearchEngine): ResultList => {
return buildResultList(engine, {
options: {
fieldsToInclude: ['page_description'],
},
});
};
By default fieldsToInclude
when not specified will only include the default fields of Coveo, any custom fields added will not be there by default. This will be important, especially for results where you have to show more information of each item.
Once we have our resultController
function setup, we can now setup our ResultList component. It will look pretty overwhelming but I’ll walk you through the important parts.
import { useEffect, useState } from 'react';
import {
Result,
buildResultTemplatesManager,
ResultTemplatesManager,
SearchEngine,
ResultList as CoveoResultList,
} from '@coveo/headless';
type Template = (result: Result) => React.ReactNode;
interface FieldValueInterface {
value: string;
caption: string;
style: string;
}
const ResultList = ({
controller,
engine,
}: {
controller: CoveoResultList;
engine: SearchEngine;
}): JSX.Element => {
const [state, setState] = useState(controller.state);
const headlessResultTemplateManager: ResultTemplatesManager<Template> =
buildResultTemplatesManager(engine);
headlessResultTemplateManager.registerTemplates({
conditions: [],
content: (result: Result) => {
return (
<div>{result.title}</div>
);
},
});
useEffect(() => {
controller.subscribe(() => setState(controller.state));
}, [controller]);
if (!state.results.length) {
return <></>;
}
return (
<div className="w-full">
{state.results.map((result: Result) => {
const template = headlessResultTemplateManager.selectTemplate(result);
return template ? template(result) : null;
})}
</div>
);
};
export default ResultList;
To start off, let’s look at this piece of code, this is pretty useful especially for cases where the resulting design may vary depending on some sort of type, more documentation can be found on the official docs of Coveo.
...
const headlessResultTemplateManager: ResultTemplatesManager<Template> =
buildResultTemplatesManager(engine);
headlessResultTemplateManager.registerTemplates({
conditions: [],
content: (result: Result) => {
return (
<div>{result.title}</div>
);
},
});
...
Since we don’t have any other templates, we’ll just leave the conditions empty and it’ll be used like this.
return (
<div className="w-full">
{state.results.map((result: Result) => {
const template = headlessResultTemplateManager.selectTemplate(result);
return template ? template(result) : null;
})}
</div>
);
There are a lot of cool features on Coveo’s ResultList, from highlighting text based on search queries, to adding URI and more. You can check those out on the documentation and if you’re lucky we might write future blogs dedicated to more components that can be used on Coveo Headless.
With our ResultList finished, we can finally add a new component to our SearchPage, nothing too complicated at this point, just import and plug in the right props needed.
Conclusion
If this is your first time diving into Coveo everything might be overwhelming, but slowly practicing and familiarizing the documentation and different headless components, you will get there. Some tips I can give would probably start off with knowing what are the requirements you will need to accomplish with Coveo Headless. Due to how modularized everything is, you’ll find the right component for your needs.
Here are some handful links from Coveo that will be useful:
- Introduction to Coveo Headless
- Coveo Headless Components / Controllers
- Github Demo Coveo Headless with Next.js