Insights

Building Components with Coveo Headless Controllers in Nextjs

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:



👋 Hey Sitecore Enthusiasts!

Sign up to our bi-weekly newsletter for a bite-sized curation of valuable insight from the Sitecore community.

What’s in it for you?

  • Stay up-to-date with the latest Sitecore news
  • New to Sitecore? Learn tips and tricks to help you navigate this powerful tool
  • Sitecore pro? Expand your skill set and discover troubleshooting tips
  • Browse open careers and opportunities
  • Get a chance to be featured in upcoming editions
  • Learn our secret handshake
  • And more!
Sitecore Snack a newsletter by Fishtank Consulting
 

Meet John Flores

Front-End Developer

🪴🐩📷

John is a Front-End Developer who is passionate about design and development. Outside of work, John has wide range of hobbies, from his plant collection, being a dog daddy, and a foodie.

Connect with John