You can add Algolia faceted search to Webflow CMS content with a Next.js app on Webflow Cloud, with filters, highlighting, pagination, and webhook sync.
Webflow's native site search covers your whole site, but it doesn't let visitors filter results by category, tag, date, or other attributes, and it offers little control over typo tolerance and relevance. Algolia adds that faceted, instant-search layer on top of your Webflow CMS content.
I've used it on Webflow projects, and the pattern I reach for in every Webflow Cloud setup is the same: react-instantsearch for the search UI, mounted in a Next.js Client Component; a Route Handler for bulk indexing Webflow CMS content; and a webhook endpoint that keeps the index current without manual intervention.
In this guide, we explore how to build that setup from scratch: configuring the Algolia index, indexing Webflow CMS content via the Data API, rendering a faceted search UI with filtering, highlighting, and pagination, and wiring up webhooks to keep the index in sync as content changes.
What do you need to build a faceted search interface with Algolia and Webflow?
The baseline you need is a Webflow Cloud Next.js app and an Algolia account. The Algolia free plan (1 million records, 10,000 searches/month) is enough to build and test everything here.
Here are the full prerequisites:
- A Webflow Cloud project with a Next.js app deployed or running locally, mounted at a path on your Webflow site
- An Algolia account (free tier at algolia.com) with an application created
- A Webflow CMS collection with the content you want to make searchable (blog posts, products, directory listings)
- A Webflow API token with
cms:readandsites:readpermissions, generated in Webflow under Site settings, then Apps & integrations, then API access
One architectural decision to get clear before writing code is that the search-only API key is safe in frontend JavaScript (it can query but not modify records). The Admin API key must never leave the server. I keep the Admin key in the environment variables on the Route Handler, and only the search-only key is sent to the browser.
7 steps to build a faceted search with Algolia on Webflow Cloud
The architecture splits cleanly into two layers:
- The indexing layer (Route Handlers) runs server-side on Cloudflare Workers using the Admin API key
- The search UI layer (React Client Components) runs in the browser using only the search-only key.
react-instantsearchhandles state management across the search box, facet filters, pagination, and results display, so I'm not coordinating those manually
Let’s dive into the steps.
1. Create the Algolia index and gather your API keys
Log in to the Algolia dashboard and create a new index. I name it after the content type I'm indexing, something like webflow-resources or webflow-products, so it's identifiable across projects.
From the dashboard, open Settings > API Keys.
You need three values:
- Application ID: Used in both server and client code
- Search-Only API Key: Safe for client-side code, can only query
- Admin API Key: Server-side only, never in frontend code
Keep these three values together, because the rest of the build depends on routing each one to the right place. The Application ID and search-only key are sent to the browser, while the Admin API key stays on the server in your Route Handlers. With the index created and the keys in hand, the next step is to install the packages and wire these values into your environment.
2. Install the packages and set environment variables
Install both Algolia packages in your Webflow Cloud Next.js project:
npm install algoliasearch react-instantsearch
algoliasearch covers both the lite search client (for the browser) and the full client (for indexing in Route Handlers). react-instantsearch provides the UI widgets: InstantSearch, SearchBox, Hits, RefinementList, Pagination, and Highlight.
Then, add the credentials to .env.local:
# .env.local
NEXT_PUBLIC_ALGOLIA_APP_ID=your_application_id
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=your_search_only_api_key
NEXT_PUBLIC_ALGOLIA_INDEX_NAME=webflow-resources
ALGOLIA_ADMIN_KEY=your_admin_api_key
ALGOLIA_INDEX_NAME=webflow-resources
WEBFLOW_API_TOKEN=your_webflow_api_token
WEBFLOW_COLLECTION_ID=your_collection_id
INDEXING_SECRET=your_random_indexing_secret
Notice that ALGOLIA_ADMIN_KEY does NOT have a NEXT_PUBLIC_ prefix. Anything prefixed with NEXT_PUBLIC_ is bundled into the client-side JavaScript. The admin key must stay server-side only. I've seen this mistake in production: the admin key in the frontend bundle is exposed to anyone who opens DevTools.
Add all of these to your Webflow Cloud environment under Environments > Environment Variables before deploying. The public index name powers the browser search client, while the unprefixed index name and indexing secret are read only by the server-side Route Handlers.
3. Configure searchable attributes and facets in Algolia
Before indexing any records, configure the index settings. This tells Algolia which fields are searchable and which can be used as facet filters. I always do this before the first index, not after, so the initial sync lands in an already-configured index.
In the Algolia dashboard, open your index and go to Configuration > Searchable attributes. Add the fields you want to search. For a blog post index, I typically add title, excerpt, tagsand author. Drag them into priority order so the title matches the rank above, and the excerpt matches.
Then go to Configuration > Facets. Add the attributes visitors will filter by. For faceted search to work, these attributes must be in the Algolia record AND listed here. Common choices for a content library: category, tags, author. For a product catalog: brand, price_range, in_stock.
You can also configure these settings via the API in your indexing Route Handler, which I'll show in Step 5.
4. Build the bulk indexing Route Handler
The indexing Route Handler fetches all published items from your Webflow CMS collection and pushes them to Algolia. I run this once to seed the index, and the webhook handler in Step 6 takes over from there.
Create app/api/algolia/index/route.ts:
// app/api/algolia/index/route.ts
import { algoliasearch } from 'algoliasearch'
import { NextResponse, type NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
// Simple shared secret to prevent unauthorized re-indexing
const authHeader = request.headers.get('authorization')
if (authHeader !== `Bearer ${process.env.INDEXING_SECRET}`) {
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
}
const client = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!
)
// Fetch all published items from Webflow CMS
let items: Record<string, unknown>[] = []
let offset = 0
const limit = 100
while (true) {
const res = await fetch(
`https://api.webflow.com/v2/collections/${process.env.WEBFLOW_COLLECTION_ID}/items?limit=${limit}&offset=${offset}`,
{
headers: {
Authorization: `Bearer ${process.env.WEBFLOW_API_TOKEN}`,
},
}
)
if (!res.ok) {
return NextResponse.json({ error: 'Webflow fetch failed' }, { status: 500 })
}
const { items: page, pagination } = await res.json() as {
items: Record<string, unknown>[]
pagination: { limit: number; offset: number; total: number }
}
items = items.concat(page)
if (offset + limit >= pagination.total) break
offset += limit
}
// Map Webflow items to Algolia records
// objectID must be the Webflow item id. This is what lets webhook
// updates and deletes find the right record without creating duplicates.
const records = items.map((item) => {
const fields = item.fieldData as Record<string, unknown>
return {
objectID: item.id as string,
title: fields.name as string,
excerpt: fields.excerpt as string ?? '',
category: fields.category as string ?? '',
tags: fields.tags as string[] ?? [],
author: fields.author as string ?? '',
slug: fields.slug as string ?? '',
publishedAt: item.lastPublished as string ?? '',
}
})
// Push to Algolia in batches
await client.saveObjects({
indexName: process.env.ALGOLIA_INDEX_NAME!,
objects: records,
})
return NextResponse.json({ indexed: records.length })
}
I trigger this once via curl -X POST https://yoursite.webflow.io/app/api/algolia/index -H "Authorization: Bearer YOUR_SECRET" after deploying.
The INDEXING_SECRET is a random string I set in the environment variables. It's not a security-hardened approach, but it prevents accidental reindexing caused by stray requests.
However, there are two things to watch here. Webflow's CMS API paginates at 100 items per page, so the while loop is not optional for collections larger than 100. And Algolia's average record size limit is 10 KB. If your CMS items have long rich-text fields, strip the HTML and index only the excerpts.
5. Build the faceted search UI
The search UI is a React Client Component in your Next.js Webflow Cloud app. I mount it on a dedicated /search page within the app, which visitors reach at something like yoursite.webflow.io/app/search.
Create app/search/SearchUI.tsx as a Client Component, since react-instantsearch manages browser state:
// app/search/SearchUI.tsx
'use client'
import { liteClient as algoliasearch } from 'algoliasearch/lite'
import {
InstantSearch,
SearchBox,
Hits,
Highlight,
RefinementList,
Pagination,
Configure,
CurrentRefinements,
Stats,
} from 'react-instantsearch'
import 'instantsearch.css/themes/satellite.css'
const searchClient = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!
)
function Hit({ hit }: { hit: Record<string, unknown> }) {
return (
<article className="hit-card">
<a href={`/blog/${hit.slug}`}>
<h3>
<Highlight attribute="title" hit={hit} />
</h3>
<p>
<Highlight attribute="excerpt" hit={hit} />
</p>
<div className="hit-meta">
<span>{hit.category as string}</span>
<span>{hit.author as string}</span>
</div>
</a>
</article>
)
}
export default function SearchUI() {
return (
<InstantSearch
searchClient={searchClient}
indexName={process.env.NEXT_PUBLIC_ALGOLIA_INDEX_NAME!}
insights
>
{/* hitsPerPage controls results per page, clickAnalytics enables event attribution */}
<Configure hitsPerPage={12} clickAnalytics />
<div className="search-layout">
<aside className="facets-panel">
<h3>Category</h3>
<RefinementList attribute="category" />
<h3>Tags</h3>
<RefinementList attribute="tags" searchable limit={10} />
<h3>Author</h3>
<RefinementList attribute="author" />
</aside>
<div className="results-panel">
<SearchBox placeholder="Search resources..." autoFocus />
<div className="results-meta">
<Stats />
<CurrentRefinements />
</div>
<Hits hitComponent={Hit} />
<Pagination />
</div>
</div>
</InstantSearch>
)
}
Then render it from the page Server Component:
// app/search/page.tsx
import SearchUI from './SearchUI'
export default function SearchPage() {
return (
<main>
<h1>Search Resources</h1>
<SearchUI />
</main>
)
}
I always add insights to <InstantSearch> from day one, not as an afterthought. Setting clickAnalytics in <Configure> means Algolia returns the queryID in every response, the key that ties click events to the specific search that triggered them. Without it, your analytics dashboard shows zero-click data, and recommendation models won't train properly.
The liteClient import (algoliasearch/lite) is intentional for the browser. It's a smaller bundle that only supports search with no indexing methods. The full algoliasearch client ships the Admin API surface area too, which has no place in frontend code.
6. Build the webhook Route Handler for real-time sync
After the initial bulk index, webhooks keep the index current. Every time a Webflow CMS item is created, updated, published, or deleted, Webflow fires an event that hits this Route Handler.
Create app/api/algolia/webhook/route.ts:
// app/api/algolia/webhook/route.ts
import { algoliasearch } from 'algoliasearch'
import { NextResponse, type NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const { triggerType, payload } = await request.json() as {
triggerType: string
payload: {
id: string
fieldData: Record<string, unknown>
lastPublished: string | null
}
}
const client = algoliasearch(
process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
process.env.ALGOLIA_ADMIN_KEY!
)
const indexName = process.env.ALGOLIA_INDEX_NAME!
if (triggerType === 'collection_item_deleted' || triggerType === 'collection_item_unpublished') {
await client.deleteObject({ indexName, objectID: payload.id })
return NextResponse.json({ action: 'deleted', objectID: payload.id })
}
if (triggerType === 'collection_item_created' || triggerType === 'collection_item_changed') {
const fields = payload.fieldData
const record = {
objectID: payload.id,
title: fields.name as string,
excerpt: fields.excerpt as string ?? '',
category: fields.category as string ?? '',
tags: fields.tags as string[] ?? [],
author: fields.author as string ?? '',
slug: fields.slug as string ?? '',
publishedAt: payload.lastPublished ?? '',
}
await client.saveObject({ indexName, body: record })
return NextResponse.json({ action: 'saved', objectID: payload.id })
}
return NextResponse.json({ action: 'ignored', triggerType })
}
Register the webhook through the Webflow Data API, since the dashboard only exposes site publish and form submission triggers. Send a POST request to the Create Webhook endpoint (https://api.webflow.com/v2/sites/{site_id}/webhooks) once per event, with triggerType set to each of the four CMS item events:
collection_item_createdcollection_item_changedcollection_item_deletedcollection_item_unpublished
Point the url of each at https://yoursite.webflow.io/app/api/algolia/webhook. Creating them via the API also gives you the x-webflow-signature header if you want to validate payloads later.
One thing I verify early is that the webhook payload properties match what the Webflow API actually sends. The docs show isDraft/isArchived in field tables, but live payloads often use draft/archived. I always test with a real item before trusting the property names.
7. Deploy and add the search page to Webflow
Deploy to Webflow Cloud with webflow cloud deploy or by pushing to your connected GitHub branch. Once the deployment completes, run the indexing script via cURL to seed the index.
To surface the search page in your Webflow site, create a Webflow page at a path like /search that embeds an iframe or uses a link to redirect to the Cloud app's search mount path. The cleaner approach is to mount the Cloud app directly at /search in your Webflow Cloud environment settings. This way, visitors land at yoursite.webflow.io/search, and the Next.js app handles it natively.
To include the Algolia satellite CSS theme (which styles the widgets), add the CDN link to your Next.js layout or import it from the npm package in the Client Component's parent layout:
// app/layout.tsx
import 'instantsearch.css/themes/satellite.css'
Or add it to Webflow's Site Settings > Custom Code > Head code if you're loading the search page as a separate experience.
What causes searches with Algolia to fail on Webflow Cloud?
The search UI usually works the first time. These three issues show up when the indexing or sync isn't behaving as expected.
RefinementList renders no options
This means the attribute isn't in attributesForFaceting on the index, even if it exists in the records. The facet must be configured in Algolia before InstantSearch can filter by it. Go to Algolia dashboard > Configuration > Facets and add the attribute. In my experience, forgetting to add tags as a facet while it's in the records is the most common cause.
Deleted Webflow items still appear in search results
The webhook for collection_item_unpublished or collection_item_deleted didn't fire or wasn't registered. Call the List Webhooks endpoint (GET https://api.webflow.com/v2/sites/{site_id}/webhooks) and confirm all four events are registered with the correct URL.
If a webhook fires but the Route Handler returns an error, Webflow retries a few times then stops. Check your Webflow Cloud build logs for 500 errors on the webhook endpoint.
Record size errors on indexing
Algolia rejects records that exceed its size limits when you're indexing full CMS rich text fields. The fix is to strip HTML and truncate body text to 500-1,000 characters in the record mapping. I keep the full slug for linking and the excerpt or a truncated version of the body for searching. Visitors don't need to see 5,000 words in a search result card anyway.
Build more on top of this
The index you've built here is the foundation for cross-collection search (querying multiple indices in a single InstantSearch instance), autocomplete in your Webflow navbar, and Algolia Recommend widgets on content template pages.
Explore Webflow + Algolia for the full picture of what Algolia connects to in Webflow. For deeper customization of your Webflow Cloud app beyond what the standard Next.js setup provides, Webflow's developer docs cover storage, environments, and deployment options in detail.
Frequently asked questions
Can I search across multiple Webflow CMS collections at once?
Yes. Add an <Index> component inside <InstantSearch> for each additional index, or use Algolia's multi-query endpoint to federate results from multiple indices into a single search UI. I build one Algolia index per Webflow collection and use react-instantsearch's Index widget to combine them when I need cross-collection search.
Does this work with Webflow's native Ecommerce products?
Yes, but you need to use the Webflow Data API to fetch product data via the /v2/sites/{site_id}/products endpoint rather than a CMS collection endpoint. The indexing Route Handler pattern is the same; only the fetch URL and field mapping change. Price filtering works best with Algolia's RangeInput or RangeSlider widgets configured against a numeric price attribute.
Is the search-only API key actually safe to expose in frontend code?
Yes, by design. Algolia generates separate key types for this reason. The search-only key can query records but cannot add, modify, or delete them. I add an HTTP Referrer restriction in the Algolia dashboard so the key only accepts requests from my site's domain. That makes it safe even if someone copies the key from the page source.
What plan does Webflow Cloud require?
Webflow Cloud is available across Webflow site plans, including the free Starter plan for development on a webflow.io URL. Mounting your app to a custom domain requires a Premium Site plan or higher, so check Webflow's pricing for the current tiers. Algolia's free plan includes 1 million records and 10,000 searches per month, which covers most development and early production workloads.




