What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
What is your developer-dependent CMS actually costing you? Calculate your costs and get a full report
Search
Index Agility content in Algolia with signed webhooks and the v5 client, then build search with React InstantSearch — plus Algolia's AI options.
Algolia is a hosted search API that returns results in milliseconds, with ready-made UI libraries for React, Vue and JavaScript. Agility manages and publishes your content. Algolia helps your visitors find it.
This guide builds on Indexing Agility Content for Search. Read that first. It covers the webhook, signature verification, re-fetching from the Fetch API, deletes and the full reindex, which are the same for every provider. This guide adds the Algolia-specific parts.
Following the previous version of this guide? It used the v4
algoliasearchclient, which Algolia stopped supporting on 14 August 2026, and it didn't verify webhook signatures. See Upgrading from the previous guide.
addObject, deleteObject and settings permissions for your webhook. Don't use the Admin key.npm install algoliasearch react-instantsearch react-instantsearch-nextjs
# .env.local
ALGOLIA_APP_ID=...
ALGOLIA_WRITE_API_KEY=... # server only: webhook + reindex
ALGOLIA_INDEX=agility_content
NEXT_PUBLIC_ALGOLIA_APP_ID=... # the same Application ID, exposed to the browser
NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY=... # the Search API key — safe in the browser
Run this once (and again whenever you change it). It tells Algolia which attributes to search, in what order of importance, and which ones you'll filter on:
// scripts/configure-algolia.ts
import { algoliasearch } from "algoliasearch"
const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_API_KEY!)
await client.setSettings({
indexName: process.env.ALGOLIA_INDEX ?? "agility_content",
indexSettings: {
searchableAttributes: ["title", "description", "body"], // earlier = more important
attributesForFaceting: ["filterOnly(locale)", "kind", "referenceName"],
attributesToSnippet: ["body:30"],
customRanking: ["desc(updatedAt)"], // on a tie, prefer newer content
},
})
For more than one locale, consider one index per locale (agility_content_en-us) so you can set language-specific settings such as queryLanguages and indexLanguages. Otherwise, filter on locale as shown below.
This is the searchIndex object the webhook route and the reindex script import:
// lib/search/provider.ts
import { algoliasearch } from "algoliasearch"
import type { SearchRecord } from "./types"
const client = algoliasearch(process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_WRITE_API_KEY!)
const indexName = process.env.ALGOLIA_INDEX ?? "agility_content"
// Keep records small: 10 KB is the maximum on the Free plan
const MAX_BODY = 8000
const toObject = (r: SearchRecord) => ({
...r,
objectID: r.id,
body: r.body.slice(0, MAX_BODY),
updatedAt: Math.floor(Date.parse(r.updatedAt) / 1000), // numeric, so it can be used for ranking
})
export const searchIndex = {
async upsert(record: SearchRecord) {
await client.saveObject({ indexName, body: toObject(record) })
},
async upsertMany(records: SearchRecord[]) {
if (records.length) await client.saveObjects({ indexName, objects: records.map(toObject) })
},
async remove(id: string) {
await client.deleteObject({ indexName, objectID: id })
},
}
Plug this into the route handler and reindex script from the indexing guide, and publishing in Agility now updates Algolia.
A few things to know:
taskID as soon as Algolia has accepted the change, and the record is searchable a moment later. There's no need to wait in a webhook handler. When a script needs the change to be live before continuing, use client.waitForTask({ indexName, taskID }).objectID of ${id}-${n} and a shared parentId, and set distinct: true with attributeForDistinct: "parentId" so each item appears once in results. See Indexing long documents. If you do this, remove must delete every chunk. Use client.deleteBy({ indexName, deleteParams: { filters: parentId:"${id}" } }) with parentId in attributesForFaceting.For a clean rebuild, replaceAllObjects writes everything to a temporary index and then swaps it in atomically, so search never shows a half-built index:
await client.replaceAllObjects({ indexName, objects: allRecords.map(toObject) })
Collect allRecords with the Sync API loop from the indexing guide. Be aware that it copies your index settings, rules and synonyms along with the records, and a very large rebuild temporarily doubles your record count.
React InstantSearch gives you a search box, results, highlighting, facets and pagination as components. With the Next.js App Router, use InstantSearchNext so results are server-rendered on the first load:
// components/SiteSearch.tsx
"use client"
import Link from "next/link"
import { liteClient as algoliasearch } from "algoliasearch/lite"
import { Configure, Highlight, Hits, SearchBox, Snippet } from "react-instantsearch"
import { InstantSearchNext } from "react-instantsearch-nextjs"
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_API_KEY!,
)
function Hit({ hit }: { hit: any }) {
return (
<Link href={hit.url}>
<Highlight attribute="title" hit={hit} />
<Snippet attribute="body" hit={hit} />
</Link>
)
}
export function SiteSearch({ locale }: { locale: string }) {
return (
<InstantSearchNext searchClient={searchClient} indexName="agility_content" insights>
<Configure filters={`locale:"${locale}"`} hitsPerPage={10} />
<SearchBox placeholder="Search…" />
<Hits hitComponent={Hit} />
</InstantSearchNext>
)
}
// app/[locale]/search/page.tsx
import { SiteSearch } from "@/components/SiteSearch"
export const dynamic = "force-dynamic"
export default async function SearchPage({ params }: { params: Promise<{ locale: string }> }) {
const { locale } = await params
return <SiteSearch locale={locale} />
}
<Autocomplete> widget (stable since react-instantsearch 7.41). It replaces the separate @algolia/autocomplete-* packages the previous version of this guide used, which Algolia no longer recommends for new projects.InstantSearch in InstantSearchSSRProvider and use getServerState. See server-side rendering.insights prop sends click and conversion events, which power Algolia's analytics, AI ranking and Recommend.The Search API key is designed to be public, and it can only search. Anyone can still copy it and query your index directly, bypassing your Configure filters. That matters in two cases:
const securedKey = client.generateSecuredApiKey({
parentApiKey: process.env.ALGOLIA_SEARCH_API_KEY!, // a search-only key, never the Admin key
restrictions: { filters: `locale:"en-us"`, validUntil: Math.floor(Date.now() / 1000) + 3600 },
})
<Chat> widget. It's available on every plan. For production you connect your own LLM provider (Anthropic, OpenAI, Azure OpenAI, Gemini and others). See Agent Studio. Ask AI is now part of Agent Studio, so build new assistants on Agent Studio rather than the older Ask AI API.Plan features change. See Algolia pricing for what's included in each plan.
The webhook integration gives you structured records, updated seconds after publishing. Algolia also offers:
saveObjectsWithTransformation). This is useful when non-developers need to change what gets indexed.If you built from the earlier version of this page:
Upgrade to algoliasearch v5. initIndex is gone, and every method takes indexName instead:
| v4 | v5 |
|---|---|
import algoliasearch from "algoliasearch" | import { algoliasearch } from "algoliasearch" |
import algoliasearch from "algoliasearch/lite" | import { liteClient as algoliasearch } from "algoliasearch/lite" |
index.saveObject(obj) | client.saveObject({ indexName, body: obj }) |
index.saveObjects(objs) | client.saveObjects({ indexName, objects: objs }) |
index.deleteObject(id) | client.deleteObject({ indexName, objectID: String(id) }) |
index.search(query) | client.searchSingleIndex({ indexName, searchParams: { query } }) |
See Algolia's upgrade guide.
Verify signatures. Turn on secure delivery for your webhook and verify it, as in the indexing guide.
Handle unpublishing. Agility sends Deleted for both unpublish and delete. The earlier example only handled deletes, and it never sent a response in that branch, so the request hung until it timed out.
Use locale-aware object IDs. The earlier example used the bare contentID, so locales overwrote each other. Switching to en-us-content-39 style IDs means a full reindex into a fresh index. Use replaceAllObjects.
Move to InstantSearch for the UI. <Link><a>…</a></Link> also no longer works in current Next.js; Link renders the <a> itself.
Run the full reindex once. The webhook only indexes content published after it was set up.
MAX_BODY, or split records as described above.addObject or deleteObject, or it's restricted to a different index.searchableAttributes includes the fields and that your Configure filters value matches the record's locale.Contact us to talk to an expert about this integration.