Web Studio is here! An enhanced experience to make it easier to create, preview, and collaborate on your website contentLearn More
Algolia search enables next generation apps with APIs that deliver relevant content in milliseconds.
While Agility CMS allows you to manage and publish content with ease, Algolia helps your users find the content they are looking for.
This guide will walk you through how to set up the integration from start to finish.
You'll need to have access to an Agility instance and an Algolia account in order to get started.
In Agility, you'll need to have an idea of what content you want to index and have available to your Algolia search results.
The two most common ways you may integrate are:
referenceNames
to look for later (when processing a content change Webhook).In order for Algolia to be aware of your content, you must submit content to the index in Algolia. To do this, you'll need to have an API route that can be called by Agility (via a Webhook) whenever content is updated in the CMS.
The role of this API route will be to:
You'll also need to initialize the index in Algolia so that you can add objects to it.
In the below example, this represents an API route that looks for changes to a specific content type such as an Article. It will only process indexing for articles.
The example provided was built using Next.js, uses the Algolia JS SDK, and uses GraphQL to lookup content from Agility. It adds objects to the index called doc_site
.
import algoliasearch from "algoliasearch";
import { client } from 'agility-graphql-client';
import { gql } from "@apollo/client";
import { getDynamicPageURL } from "@agility/nextjs/node";
import { normalizeArticle } from "utils/searchUtils";
//set up the Agolia client and index
const algoliaClient = algoliasearch(process.env.ALGOLIA_APP_ID, process.env.ALGOLIA_ADMIN_API_KEY);
const index = algoliaClient.initIndex("doc_site");
export default async (req, res) => {
//check the referenceName of the content being updated
const referenceName = req.body.referenceName;
//if our content is not an article, do nothing
if(!referenceName || !referenceName.endsWith('articles')) {
//kickout
res.status(200).end();
return;
}
const contentID = req.body.contentID;
const state = req.body.state;
//if the article has been deleted or unpubilshed, remove it from the index
if(contentID && state && state === 'Deleted') {
await index.deleteObject(contentID);
return;
} else {
//use GraphQL to lookup the details or the article from Agility
const { data } = await client.query({
query: gql`
{
${referenceName} (contentID: ${contentID}) {
contentID
properties {
itemOrder
}
fields {
title
content
description
section {
contentID
fields {
title
}
}
concept {
contentID
fields {
title
}
}
}
}
}`,
});
const article = data[referenceName][0];
//resolve the URL for the article
const url = await getDynamicPageURL({
contentID: article.contentID,
preview: false
})
//format the article into the object we want to index
const object = await normalizeArticle({
article,
url
})
//save it in Algolia
await index.saveObject(object)
}
res.status(200).json();
};
Once you have your API route functioning and you are indexing content in Algolia, you'll want to deploy this to a public endpoint so that Agility can call it with a webhook.
Now that you have a deployed endpoint to handle indexing content to Algolia, next you'll need to set up a Webhook in Agility so that Agility can call this endpoint when content changes.
One of the challenges with Webhooks is that by default, Agility CMS cannot communicate directly with your localhost app. This can make it difficult to test and debug issues.
One way to get around this is to use a localhost
tunnel proxy which can proxy requests to a public endpoint to your localhost
so you can run logic locally and debug it. We recommend using the service Ngrok which is free and provides a temporary public URL to proxy to your localhost
.
Once you have created an Ngrok Url, you can then add another Webhook in Agility (using your Ngrok Url) which will run code on your localhost
when the appropriate events fire.
To verify that your logic is successfully indexing content, log in to Algolia and inspect your index(es). Verify the information there is correct. You can also set up a search test within the Algolia app to actually test how the search works against your index.
Once you have confirmed your content is being indexed correctly, the last step is to actually provide a frontend UI for your users to input words and query Algolia for the search results.
Alogila offers SDKs in a variety of programming languages/frameworks to help you get started with building a search UI.
The following example is what an Autocomplete search UI may look like in a Next.js React app.
import React from 'react'
import Link from 'next/link'
import { ChevronRightIcon } from '@heroicons/react/outline'
import algoliasearch from 'algoliasearch/lite';
import { createAutocomplete } from '@algolia/autocomplete-core';
import { getAlgoliaResults } from '@algolia/autocomplete-preset-algolia';
import { renderHTML } from '@agility/nextjs'
const searchClient = algoliasearch(process.env.NEXT_PUBLIC_ALGOLIA_APP_ID, process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY);
const Search = () => {
// (1) Create a React state.
const inputRef = React.useRef(null)
const [autocompleteState, setAutocompleteState] = React.useState({});
const autocomplete = React.useMemo(
() =>
createAutocomplete({
onStateChange({ state }) {
// (2) Synchronize the Autocomplete state with the React state.
setAutocompleteState(state);
},
getSources() {
return [
// (3) Use an Algolia index source.
{
sourceId: 'articles',
getItemInputValue({ item }) {
return item.query;
},
getItems({ query }) {
return getAlgoliaResults({
searchClient,
queries: [
{
indexName: 'doc_site',
query,
params: {
hitsPerPage: 4,
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
},
},
],
});
},
getItemUrl({ item }) {
return item.url;
},
},
];
},
}),
[]
);
return (
<div className="aa-Autocomplete" {...autocomplete.getRootProps({})}>
<form
className="aa-Form"
{...autocomplete.getFormProps({ inputElement: inputRef.current })}
>
<input
className="block w-full bg-white border border-gray-300 rounded-md py-2 pl-10 pr-3 text-sm placeholder-gray-500 focus:outline-none focus:text-gray-900 focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
{...autocomplete.getInputProps({})}
placeholder="Search docs..."
/>
</form>
<div className="aa-Panel relative" {...autocomplete.getPanelProps({})}>
{autocompleteState.isOpen &&
autocompleteState.collections.map((collection, index) => {
const { source, items } = collection;
return (
<div key={`source-${index}`} className="aa-Source absolute bg-white z-50 border border-gray-300 w-full">
{items.length > 0 && (
<ul className="aa-List" {...autocomplete.getListProps()}>
{items.map((item) => {
let description = null;
if(item._snippetResult?.content && item._snippetResult.content.length > 0) {
let snippetBlock = item._snippetResult.content.find((block) => {
return block.data.text.matchLevel === "full";
})
if(snippetBlock) {
description = snippetBlock.data.text.value;
}
}
if(!description) {
description = item._highlightResult.description.value
}
return (
<li
key={item.objectID}
className="aa-Item"
{...autocomplete.getItemProps({
item,
source,
})}
>
<Link href={item.url}>
<a className="SearchResult px-5 py-2 block w-full hover:bg-gray-50">
<span className="SearchResult__titlee block text-indigo-600 text-sm font-bold" dangerouslySetInnerHTML={renderHTML(item._highlightResult.title.value)}></span>
{description &&
<span className="SearchResult__description block text-sm mb-2" dangerouslySetInnerHTML={renderHTML(description)}></span>
}
<span className="SearchResult__category-section block text-xs font-light text-gray-600">
{item.category} <ChevronRightIcon className="inline w-2"/> {item.section}
</span>
</a>
</Link>
</li>
)})}
</ul>
)}
</div>
);
})}
</div>
</div>
);
}
export default Search;
Contact us to learn more about this integration and speak with an expert.