Algolia ×
Webflow
A drop-in Algolia integration for Webflow, driven entirely by HTML data-* attributes. Powers two features — Filtering (render any CMS collection past Webflow's 100-item cap with full-text search and facets) and Federated search (one site-wide index across all collections and static pages).
Real-time data sync
Three parts, two features
The same three-part architecture powers both filtering and federated search. Each piece is reusable across projects with isolated configuration.
Next.js on Webflow Cloud
Sync App
Next.js on Webflow Cloud. Exposes two endpoints: /api/sync for Feature A (one CMS collection → filter index) and /api/search-all for Feature B (many collections + static pages → federated index).
Cloudflare Worker
Webhook Worker
Listens for Webflow webhooks and keeps Algolia in sync incrementally — per-item changes on create/edit/delete, and triggers a full re-sync on site publish.
Vanilla JS via jsDelivr
Client Library
A single <script> tag in Webflow. Reads HTML data-* attributes, queries Algolia, and renders results. Powers both filtering (Feature A) and federated search with autosuggest (Feature B).
Get running in three tracks
Start with the six shared foundation steps, then follow Feature A, Feature B, or both.
Prerequisites
Do these six steps once, regardless of which features you use.
Go to github.com/felixeallan/algolia-webflow-filter and click "Use this template" → "Create a new repository". Name it (e.g. my-site-algolia) and make it public (required so jsDelivr can serve the script). Done — you have your own copy.
Sign up at algolia.com and create an application. Go to Settings → API Keys and copy these three values:
- Application ID → ALGOLIA_APP_ID
- Search API Key → used later in the script tag (safe to expose in the browser)
- Write API Key → ALGOLIA_ADMIN_API_KEY (keep secret, only used server-side)
You'll create the actual **index(es)** in the feature tracks below — Algolia creates an index automatically the first time you sync to it, so there's nothing to create manually here.
Webflow → Site Settings → Apps & Integrations → API Access → Generate API Token.
Permissions: CMS: Read (read-only is enough — the sync never writes back). Copy the token → WEBFLOW_API_TOKEN.
Collection IDs are configured per feature — a single ID for Feature A, a list for Feature B.
In your Webflow site → Site Settings → Webflow Cloud. Click "Install GitHub app" if needed, then "New app".
Configure:
- Name: anything (e.g. algolia-sync)
- Repository: your project repo from Step 1
- Directory path: apps/sync
- GitHub branch: main
- Path: /api (this becomes the URL prefix, e.g. yoursite.com/api/sync)
Once the environment is created, go to Environment Variables and add these base variables below. Click "Deploy latest commit" and wait for the deployment to go live.
You'll come back to **Environment Variables** to add feature-specific vars in Feature A (`WEBFLOW_COLLECTION_ID` + `ALGOLIA_INDEX_NAME`) and/or Feature B (`ALGOLIA_SEARCH_ALL_INDEX` + `SITE_URL`), then redeploy.
| Variable | Type |
|---|---|
| WEBFLOW_API_TOKEN | Secret |
| ALGOLIA_APP_ID | Text |
| ALGOLIA_ADMIN_API_KEY | Secret — this is the Write API Key |
| SYNC_SECRET | Secret — generate with: openssl rand -hex 32 |
The sync app handles bulk syncs; the webhook worker keeps Algolia current as editors publish/change/delete individual items. Webflow refuses to send webhooks to *.webflow.io domains, so this external worker is required.
- Go to dash.cloudflare.com → Workers & Pages → Create → Create Worker → Hello World
- Name it (e.g.
algolia-webflow-webhook) → Deploy - Click "Edit code", delete everything, and paste the contents of
apps/webhook-worker/src/index.js→ Deploy - Go to Settings → Variables and Secrets and add the base variables below. Feature A adds
ALGOLIA_INDEX_NAME+WEBFLOW_COLLECTION_ID; Feature B addsSEARCH_ALL_ENDPOINT. - Note your worker URL — looks like
https://algolia-webflow-webhook.YOUR-USERNAME.workers.dev
Register the webhooks
Webflow → Site Settings → Apps & Integrations → Webhooks → Add Webhook for each event below, all pointing at the worker URL. Leave the webhook secret field blank.
| Variable | Type |
|---|---|
| ALGOLIA_APP_ID | Text |
| ALGOLIA_ADMIN_API_KEY | Secret — the Write API Key |
| SYNC_ENDPOINT | Text — https://YOUR_SITE.webflow.io/api/sync |
| SYNC_SECRET | Secret — same as Webflow Cloud |
| Event | Purpose |
|---|---|
| collection_item_created | New items trigger a re-sync |
| collection_item_changed | Edits trigger a re-sync |
| collection_item_deleted | Deletes remove the item from the filter index |
| collection_item_unpublished | Unpublished items removed from the filter index |
| site_publish | Triggers a full re-sync of every configured endpoint |
In Webflow → Site Settings → Custom Code → Footer, add the script tag below.
Always pin to a version tag (e.g. @v0.8.13). Do not use @main — jsDelivr aggressively caches branch URLs.
The same script tag powers both features — you don't add it twice.
<script src="https://cdn.jsdelivr.net/gh/felixeallan/algolia-webflow-filter@v0.8.13/packages/library/dist/algolia-webflow.min.js"></script>Filter a collection
Render one CMS collection past Webflow's 100-item cap, with full-text search, faceted filters, range sliders, sorting, pagination, and URL state sync.
- In Webflow → CMS → click the collection you want to filter → Settings → copy the Collection ID.
- Decide an index name (e.g.
products,blog-posts,cars). - In your Webflow Cloud app → Environment Variables, add the two vars below, then click "Deploy latest commit".
- In your Cloudflare Worker → Settings → Variables, add the same two vars (so per-item deletes hit the right index and webhooks from other collections are ignored), then re-deploy the worker.
Health check — should return {"ok":true}:
| Variable | Type |
|---|---|
| WEBFLOW_COLLECTION_ID | Text — the collection you're filtering |
| ALGOLIA_INDEX_NAME | Text — the index name you chose, e.g. "products" |
curl https://YOUR_SITE.webflow.io/api/syncTrigger the first full sync. Check Algolia → your index → Browse — all your CMS items should appear there.
curl -X POST https://YOUR_SITE.webflow.io/api/sync \
-H "Authorization: Bearer YOUR_SYNC_SECRET"{ "success": true, "synced": 1234 }In Algolia → your index → Configuration:
Facets — fields you want to filter by:
- Add every attribute that will be used as a filter (e.g. category, brand, color, year)
- Reference fields show up as the referenced item's name automatically (e.g. car-brand: "Quasar")
Searchable Attributes — fields used by the search input:
- Set to Ordered mode
- Top of the list = higher relevance
- Add fields like name, title, description
Click "Review and Save settings".
Sorting in Algolia works through replica indexes — pre-sorted copies of the main index. Each user-facing sort option = one replica. Algolia keeps replicas in sync automatically.
A.4.1 — Create one replica per sort option
In Algolia → your main index → Configuration → Replicas → Create Replica. Choose Virtual replica (free, no extra storage). Name it using the format INDEX_FIELD_DIRECTION:
A.4.2 — Configure each replica's Sort-by rule
For each replica, click its name to open it → Configuration → Relevant sort → add exactly one Sort-by rule matching the replica's name → click Review and Save settings.
**Important:** Each replica must have **exactly one** Sort-by rule. Multiple rules turn additional ones into tiebreakers. One replica = one dropdown option.
A.4.3 — Add the sort dropdown to your page
Use a native <select> element (Webflow Designer → Add panel → Forms → Select). The Webflow navigation "Dropdown" component is div-based and won't fire change events.
A.4.4 — Caveat: sorting numbers stored as strings
For sorting to behave numerically, the field must be a number in Algolia. Use a Webflow Number field (not a Plain text field) for any field you plan to sort numerically.
| Sort option | Replica name |
|---|---|
| Name A → Z | cars_name_asc |
| Name Z → A | cars_name_desc |
| Price ↑ | cars_price_asc |
| Price ↓ | cars_price_desc |
| Year newest | cars_year_desc |
| Year oldest | cars_year_asc |
<select data-algolia-sort>
<option value="">Default (relevance)</option>
<option value="cars_name_asc">Name: A → Z</option>
<option value="cars_name_desc">Name: Z → A</option>
<option value="cars_price_asc">Price: Low → High</option>
<option value="cars_price_desc">Price: High → Low</option>
<option value="cars_year_desc">Year: Newest first</option>
<option value="cars_year_asc">Year: Oldest first</option>
</select>Add the wrapper div with your Algolia credentials, then place filter inputs, result list, and pagination inside it. Every attribute is documented in the Webflow Setup section.
<div data-algolia data-algolia-app-id="YOUR_APP_ID" data-algolia-api-key="YOUR_SEARCH_ONLY_KEY" data-algolia-index="cars" data-algolia-hits-per-page="12" data-algolia-url-state
>
<!-- Search input -->
<input data-algolia-search type="text" placeholder="Search...">
<!-- Filters: checkbox = multi-select (OR within group, AND between groups) -->
<label data-algolia-filter="car-brand" data-algolia-value="Quasar">
<input type="checkbox"><span>Quasar</span>
</label>
<!-- Filters: radio = single-select within the group -->
<label data-algolia-filter="color-theme" data-algolia-value="White">
<input type="radio" name="color"><span>White</span>
</label>
<!-- Dropdown filter -->
<select data-algolia-filter-select="year">
<option value="">All years</option>
<option value="2024">2024</option>
<option value="2023">2023</option>
</select>
<!-- Sort dropdown (uses Algolia index replicas) -->
<select data-algolia-sort>
<option value="">Relevance</option>
<option value="cars_price_asc">Price ↑</option>
<option value="cars_price_desc">Price ↓</option>
</select>
<!-- Clear buttons -->
<button data-algolia-clear>Clear all</button>
<button data-algolia-clear="car-brand">Clear brands</button>
<!-- Active filter tags -->
<div data-algolia-tags>
<div data-algolia-tag-template class="tag">
<span data-algolia-tag-label></span>
<span data-algolia-tag-remove>×</span>
</div>
</div>
<!-- Result list with template -->
<div data-algolia-list>
<div data-algolia-template class="card">
<img data-algolia-bind="image.url" data-algolia-attr="src">
<h3 data-algolia-bind="name"></h3>
<p data-algolia-bind="car-brand"></p>
<img data-algolia-bind="car-brand__logo.url" data-algolia-attr="src">
<p data-algolia-bind="price"></p>
<p data-algolia-bind="year"></p>
<!-- Repeat for array/multi-reference fields -->
<div data-algolia-hide-empty="authors" data-algolia-repeat="authors">
<span data-algolia-repeat-item class="author-tag"></span>
</div>
<a data-algolia-bind="slug" data-algolia-attr="href">View</a>
</div>
</div>
<!-- Stats -->
<p><span data-algolia-count></span> results</p>
<!-- Empty state -->
<div data-algolia-empty style="display:none">No results found.</div>
<!-- Pagination -->
<button data-algolia-prev>← Previous</button>
<span data-algolia-page-info></span>
<button data-algolia-next>Next →</button>
</div>Federated search
A single site-wide search index aggregating multiple CMS collections plus static pages into one normalized result set, with autosuggest and a dedicated results page.
Federated search uses a second Algolia index (commonly named search_all) populated by the POST /api/search-all endpoint. That endpoint reads every source you configure and normalizes each record — no matter which collection it came from — into the same fixed seven-field shape:
Because every record has the same shape, your search UI never changes when you add or remove a collection — it always binds to title, description, image, url, type, date. The only thing that varies per project is the config file that maps your collections into these fields.
| Variable | Type |
|---|---|
| objectID | Unique ID, prefixed per source (car__<id>, author__<id>, page__about) so IDs never collide across collections |
| title | The result's headline |
| description | Supporting text (optional per source) |
| url | Absolute link to the page, built from SITE_URL + the item's slug |
| image | Image URL as a plain string (already extracted — bind "image", NOT "image.url") |
| type | The source label (Car, Author, Page…) — used for the type filter |
| date | ISO date string from last-updated time; empty for static pages |
Open apps/sync/src/search-all-config.ts. This is the only file you edit — the sync route itself is generic. It has two arrays:
`collections` — one entry per CMS collection you want searchable. Keys:
- collectionId ✅ — Webflow → CMS → collection → Settings
- type ✅ — stored as type field; drives the type filter (e.g. "Product")
- prefix ✅ — keeps objectIDs unique across collections (e.g. "product" → product__<id>)
- urlPattern ✅ — relative path with {slug} placeholder, e.g. /products/{slug}
- titleField ✅ — Webflow field slug that becomes title
- descriptionField — optional field slug for description
- imageField — optional field slug for image
`staticPages` — pages that aren't CMS items (home, about, pricing…).
Field **slugs** are the lowercase-hyphenated API names, not the Designer display names (e.g. "Bio Summary" → `bio-summary`).
import { CollectionConfig, StaticPage } from './types';
export const collections: CollectionConfig[] = [
{
collectionId: 'YOUR_COLLECTION_ID', // Webflow → CMS → collection → Settings
type: 'Product', // stored as "type"; drives the type filter
prefix: 'product', // objectID prefix → product__<item-id>
urlPattern: '/products/{slug}', // {slug} replaced with the item's slug
titleField: 'name', // Webflow field slug for the title
descriptionField: 'summary', // optional
imageField: 'main-image', // optional
},
// …add one object per collection
]
export const staticPages: StaticPage[] = [
{ objectID: 'page__about', title: 'About', description: 'Learn about us', urlPath: '/about' },
{ objectID: 'page__pricing', title: 'Pricing', description: 'Our plans', urlPath: '/pricing' },
]In your Webflow Cloud app → Environment Variables, add the two vars below. (WEBFLOW_API_TOKEN, ALGOLIA_APP_ID, ALGOLIA_ADMIN_API_KEY, and SYNC_SECRET are already set from the foundation.)
Then click "Deploy latest commit".
| Variable | Type |
|---|---|
| ALGOLIA_SEARCH_ALL_INDEX | Text — the federated index name, e.g. "search_all" |
| SITE_URL | Text — your site's base URL, no trailing slash, e.g. https://YOUR_SITE.webflow.io |
Trigger the first full federated sync. The response breaks down what was indexed per type. Check Algolia → your search_all index → Browse to confirm the normalized records.
curl -X POST https://YOUR_SITE.webflow.io/api/search-all \
-H "Authorization: Bearer YOUR_SYNC_SECRET"{ "success": true, "synced": 1812, "breakdown": { "Product": 1786, "Author": 23, "Page": 3 } }In Algolia → your search_all index → Configuration:
Searchable Attributes (Ordered): add title then description.
Facets → Attributes for faceting: add type (required for the content-type filter on the results page).
(Optional) Sorting by date — create virtual replicas exactly as in step A.4, one Sort-by rule each:
Click "Review and Save settings".
| Sort option | Replica name |
|---|---|
| Newest first | search_all_date_desc |
| Oldest first | search_all_date_asc |
Add one variable to your Cloudflare Worker → Settings → Variables, then re-deploy the worker. Now whenever an item is created/changed or the site is published, the worker triggers a full re-sync of both the filter index and the search index.
**Why a full re-sync?** Federated records are normalized from many collections, so the worker re-runs the whole `/api/search-all` build rather than patching one record. It fires that request with `ctx.waitUntil()`, which keeps the Worker alive until the multi-second sync finishes. Make sure `SEARCH_ALL_ENDPOINT` points at the **same domain** as your live sync app.
| Variable | Type |
|---|---|
| SEARCH_ALL_ENDPOINT | Text — https://YOUR_SITE.webflow.io/api/search-all |
A federated search experience is two pieces: a standalone search box in the navbar (with optional autosuggest) and a dedicated results page.
Three things specific to federated search:
- Bind `image`, not `image.url`. The /api/search-all route extracts the image URL into a plain string, so the field is already a URL. Add data-algolia-hide-empty="image" so sources without an image don't render a broken <img>.
- `data-algolia-search-mode="empty"` keeps the results page blank until the visitor types, instead of dumping every record on load.
- Type filter values must match your config's `type` labels exactly (e.g. "Product", "Author", "Page"). These are facet filters, which is why type must be added as a facet in step B.5.
<!-- Lives outside any [data-algolia] wrapper, e.g. in the site navbar -->
<div style="position:relative">
<input type="text" placeholder="Search…" data-algolia-search-box data-algolia-app-id="YOUR_APP_ID" data-algolia-api-key="YOUR_SEARCH_KEY" data-algolia-index="search_all" data-algolia-search-action="both" data-algolia-search-target="/search"
/>
<!-- Optional autosuggest dropdown -->
<div data-algolia-autosuggest>
<div data-algolia-autosuggest-template>
<a data-algolia-autosuggest-link>
<span data-algolia-bind="title"></span>
<span data-algolia-bind="type"></span>
</a>
</div>
</div>
</div><!-- Reads ?q= from the URL automatically -->
<div data-algolia data-algolia-app-id="YOUR_APP_ID" data-algolia-api-key="YOUR_SEARCH_KEY" data-algolia-index="search_all" data-algolia-search-mode="empty" data-algolia-url-state
>
<input type="text" data-algolia-search placeholder="Search…" />
<!-- Show current query: "Results for 'tesla'" -->
<p>Results for "<span data-algolia-query></span>" — <span data-algolia-count></span> found</p>
<!-- Filter by content type -->
<label data-algolia-filter-all="type"><input type="radio" name="type"> All</label>
<label data-algolia-filter="type" data-algolia-value="Product"><input type="radio" name="type"> Products</label>
<label data-algolia-filter="type" data-algolia-value="Author"><input type="radio" name="type"> Authors</label>
<label data-algolia-filter="type" data-algolia-value="Page"><input type="radio" name="type"> Pages</label>
<!-- Optional sort (needs the replicas from B.5) -->
<select data-algolia-sort>
<option value="search_all">Relevance</option>
<option value="search_all_date_desc">Newest</option>
</select>
<div data-algolia-list>
<div data-algolia-template>
<a data-algolia-bind="url" data-algolia-attr="href">
<!-- image is a plain URL string — bind "image", NOT "image.url" -->
<img data-algolia-bind="image" data-algolia-attr="src" data-algolia-hide-empty="image">
<p data-algolia-bind="title"></p>
<p data-algolia-bind="description"></p>
<span data-algolia-bind="type"></span>
<span data-algolia-bind="date" data-algolia-bind-format="date"></span>
</a>
</div>
</div>
<div data-algolia-empty style="display:none">No results found.</div>
</div>Webflow Setup
Everything here is configured directly in Webflow — no code required. Add these data-* attributes to your Webflow elements to connect them to Algolia.
Don't set up attributes manually — clone the Webflow template
All attributes below are already configured in the template. Filtering: Standard Filter, Custom Pagination, Load More, or Range Slider Auto Bounds. Federated search: Search page, Search page + Submit button, Search page with Autosuggest, Search input, Search input + Submit button, or Search input with Autosuggest.
Required Setup
The minimum needed for both filtering and search to work.
Wrapper
Both| Attribute | On | Description |
|---|---|---|
data-algoliaRequired | any (wrapper div) | Marks the root. Everything else must be inside. |
data-algolia-app-id="..."Required | wrapper | Algolia Application ID |
data-algolia-api-key="..."Required | wrapper | Algolia Search-Only API key (safe to expose) |
data-algolia-index="..."Required | wrapper | Algolia index name |
data-algolia-hits-per-page="12" | wrapper | Number of results per page (default 12) |
data-algolia-url-state | wrapper | (optional) Sync filters/search/page to URL params for shareable links |
data-algolia-match-mode="or" | wrapper | (optional) Cross-group matching. Default = AND (item must match every group). "or" = OR (item matches any group). Ranges always stay AND with facets. |
data-algolia-search-mode="empty" | wrapper | (optional) Start with no results — the list stays empty until the user types. Use on dedicated search pages where showing all records before any input is undesirable. |
data-algolia-debounce="300" | wrapper | (optional) Debounce delay in ms for search/range inputs. Default 300. |
data-algolia-stagger="50" | wrapper | (optional) Entrance animation delay in ms between result items. Default 0 (no animation). Suggested: 30–60 subtle, 80–120 more visible. |
Filters & Search
Both| Attribute | On | Description |
|---|---|---|
data-algolia-searchRequired | <input type="text"> | Full-text search input. Debounced 300ms by default. Results update as the user types. |
data-algolia-submit | <button> / any | (optional) Adding this button switches the text query to manual mode: typing no longer searches instantly — the search runs only when the button is clicked or Enter is pressed. |
data-algolia-filter="attr" + data-algolia-value="val" | <label> + <input type="checkbox"> | Checkbox multi-select filter. Multiple checked values in the same attr are OR; across different attrs they are AND. |
data-algolia-filter="attr" + data-algolia-value="val" | <label> + <input type="radio"> | Radio single-select filter. Only one value per attr group can be active. All radios in the group must share the same name attribute. |
data-algolia-filter-all="attr" | <label> + <input type="radio"> | "All" option for a radio group. Auto-activates on load and whenever no specific value is selected in that group. |
data-algolia-filter-select="attr" | <select> | Dropdown (select) filter. An option with value="" resets the filter for that attr. |
Result List
Both| Attribute | On | Description |
|---|---|---|
data-algolia-listRequired | container | Where result items get injected. |
data-algolia-templateRequired | <div> or <template> | Cloned once per result, then injected into the list. A regular <div> is fine — it's hidden automatically. |
data-algolia-bind="field"Required | any (inside template) | Sets element text content from the Algolia hit field. Supports dot notation: image.url, car-brand__logo.url. |
data-algolia-bind="field" + data-algolia-attr="name" | any (inside template) | Sets an HTML attribute (src, href, alt, etc.) instead of text content. |
data-algolia-bind="field" + data-algolia-bind-format="date" | any (inside template) | Formats the field as a human-readable date (e.g. "June 10, 2024"). Accepts an ISO string or Unix timestamp. Renders nothing when empty or invalid — safe for records with no date. |
data-algolia-hide-empty="field" | any (inside template) | Hides this element when the bound field is empty, null, or an empty array. |
Optional
Add any of these to extend your UI.
Pagination
BothLoad More
A single button that appends the next page of results to the existing list. Auto-hides when there are no more pages.
| Attribute | On | Description |
|---|---|---|
data-algolia-load-more | any | Append next page to existing results. Auto-hides when no more pages. |
Prev / Next
Replaces the current results with the previous or next page. Standard navigation-style pagination.
| Attribute | On | Description |
|---|---|---|
data-algolia-prev | any | Go to previous page. Auto-disabled on the first page. |
data-algolia-next | any | Go to next page. Auto-disabled on the last page. |
Page counter
Displays "Page X of Y". Optional companion to Prev / Next.
| Attribute | On | Description |
|---|---|---|
data-algolia-page-info | any | Shows the current page and total page count, e.g. "Page 2 of 12". |
Numbered pages
Renders a row of clickable page-number buttons. Can be combined with Prev / Next and Page counter.
| Attribute | On | Description |
|---|---|---|
data-algolia-pages | container | Where numbered page buttons are injected. |
data-algolia-page-button-template | child of pages container | Cloned for each visible page number. Active page receives data-active="" for styling. |
data-algolia-page-dots-template | child of pages container | (Optional) Cloned to render the "…" separator when page numbers are skipped. |
data-algolia-page-siblings="2,1,1,0" | pages container | Pages shown on each side of the current page. Comma-separated per Webflow breakpoint (Desktop, Tablet, Landscape, Portrait). Default 1. |
data-algolia-page-boundaries="1,1,1,0" | pages container | Pages always shown at the start and end of the list. Same breakpoint syntax. Default 1. |
Sort
Both| Attribute | On | Description |
|---|---|---|
data-algolia-sort | <select> | Each option's value must be an Algolia index replica name (e.g. cars_price_asc). Must be a native <select> — Webflow's "Dropdown" nav component is div-based and will not work. |
Clear Buttons
Both| Attribute | On | Description |
|---|---|---|
data-algolia-clear | any | Clears ALL filters, search query, and sort selection. |
data-algolia-clear="attr" | any | Clears only the specified filter group (e.g. data-algolia-clear="car-brand"). |
Empty State
Both| Attribute | On | Description |
|---|---|---|
data-algolia-empty | any | Shown only when the current query returns zero results. Hidden otherwise. |
Global search box (navbar / standalone)
Federated searchInput attributes
A standalone search input placed outside a [data-algolia] wrapper — e.g. in the navbar. It queries its own index independently and can redirect to a search page, show an autosuggest dropdown, or both.
| Attribute | On | Description |
|---|---|---|
data-algolia-search-boxRequired | <input> | Marks this as a standalone global search input. |
data-algolia-app-id="..."Required | <input> | Algolia Application ID |
data-algolia-api-key="..."Required | <input> | Algolia Search-Only API key |
data-algolia-index="..."Required | <input> | Index to query (e.g. search_all) |
data-algolia-search-action="..." | <input> | redirect (default) · dropdown · both |
data-algolia-search-target="/search" | <input> | URL of the results page (redirect/both mode). Default /search |
data-algolia-search-param="q" | <input> | URL query param name. Default q |
Autosuggest dropdown
Place a container with [data-algolia-autosuggest] near the input (sibling, parent, or anywhere on page). Inside it, put one child template element with [data-algolia-autosuggest-template]. Use [data-algolia-bind] inside the template exactly as in the main results list.
| Attribute | On | Description |
|---|---|---|
data-algolia-autosuggest | container div | Wrapper for the dropdown. Hidden by default; shown when suggestions exist. |
data-algolia-autosuggest-template | child element | Cloned once per suggestion result. |
data-algolia-autosuggest-link | <a> inside template | Automatically gets href set to the hit's url field. |
<!-- Navbar search box -->
<div style="position:relative">
<input type="text" placeholder="Search…" data-algolia-search-box data-algolia-app-id="YOUR_APP_ID" data-algolia-api-key="YOUR_SEARCH_KEY" data-algolia-index="search_all" data-algolia-search-action="both" data-algolia-search-target="/search"
/>
<div data-algolia-autosuggest>
<div data-algolia-autosuggest-template>
<a data-algolia-autosuggest-link>
<span data-algolia-bind="title"></span>
<span data-algolia-bind="type"></span>
</a>
</div>
</div>
</div>Scroll Anchor
Both| Attribute | On | Description |
|---|---|---|
data-algolia-scroll-anchor | any | Scrolls smoothly to this element on every filter, search, sort, or pagination change. Can be inside or outside the [data-algolia] wrapper. Skipped on initial page load. |
Stats
Both| Attribute | On | Description |
|---|---|---|
data-algolia-count | any | Displays the total number of results matching the current query (e.g. 1,786). Updates on every filter change. |
data-algolia-query | any | Displays the current search query text. Handy for "Results for X" headings on a search page. Empty when there is no active query. |
Range Filters & Slider
Filtering| Attribute | On | Description |
|---|---|---|
data-algolia-range-min="attr"Required | <input type="number"> | Lower bound input of a numeric range filter. The attr must be a number field in Algolia. |
data-algolia-range-max="attr"Required | <input type="number"> | Upper bound input of a numeric range filter. |
data-algolia-range-label="Price" | range input | (optional) Custom label prefix for the active filter tag (e.g. "Price: Any – 2000"). Add to either min or max input. |
data-algolia-range-slider="attr"Required | wrapper div | Binds the slider to the same attr as the number inputs. |
data-algolia-range-slider-min="0"Required | slider wrapper | Static lower bound of the slider scale. Required unless using auto-bounds. |
data-algolia-range-slider-max="100000"Required | slider wrapper | Static upper bound. Required unless using auto-bounds. |
data-algolia-range-slider-auto-bounds | slider wrapper | (optional) Fetch the real min/max from Algolia facet stats on init. Replaces -slider-min/-slider-max. |
data-algolia-range-slider-step="100" | slider wrapper | (optional) Snap increment. Default 1. |
data-algolia-range-slider-format | slider wrapper | (optional) Format display spans using browser locale. Add a BCP 47 tag to force a locale (e.g. "fr-FR"). |
data-algolia-range-slider-trackRequired | child of slider wrapper | The track bar element. |
data-algolia-range-slider-fill | child of track | (optional) The highlighted fill between the two handles. |
data-algolia-range-slider-handle="min"Required | child of track | The lower drag handle. |
data-algolia-range-slider-handle="max"Required | child of track | The upper drag handle. |
data-algolia-range-slider-display="min" | any | Live text element showing the current lower value. |
data-algolia-range-slider-display="max" | any | Live text element showing the current upper value. |
Code examples
<div class="filter_block">
<!-- Number inputs the library reads. Keep visible for typing, or hide with display:none -->
<input type="number" data-algolia-range-min="price-number-2" data-algolia-range-label="Price" placeholder="min">
<input type="number" data-algolia-range-max="price-number-2" placeholder="max">
<div class="rangeslider_wrapper" data-algolia-range-slider="price-number-2" data-algolia-range-slider-min="0" data-algolia-range-slider-max="100000" data-algolia-range-slider-step="100" data-algolia-range-slider-format="en-US">
<div class="rangeslider_track" data-algolia-range-slider-track>
<div class="rangeslider_handle" data-algolia-range-slider-handle="min"></div>
<div class="rangeslider_handle" data-algolia-range-slider-handle="max"></div>
<div class="rangeslider_fill" data-algolia-range-slider-fill></div>
</div>
<div class="range_values">
<div>$<span data-algolia-range-slider-display="min">0</span></div>
<div>$<span data-algolia-range-slider-display="max">100,000</span></div>
</div>
</div>
</div>Active Filter Styling
Both| Attribute | On | Description |
|---|---|---|
[data-algolia-filter][data-active] | CSS selector | When a filter is selected, the library adds data-active="" to the label. Target this in Webflow's custom CSS to style the active state. |
w--redirected-checked | CSS class | For Webflow's native checkbox/radio components, the library also toggles this class for visual styling compatibility. |
/* Style active filter labels */
[data-algolia-filter][data-active] {
background: var(--blue-pale);
border-color: var(--blue);
color: var(--blue);
}
/* Style active page in numbered pagination */
[data-algolia-page-item][data-active] {
background: #2B4EF0;
color: white;
}Nested List
Render array / multi-reference fields as repeated child elements inside a result card.
Nested List (Repeat)
Filtering| Attribute | On | Description |
|---|---|---|
data-algolia-repeat="field" | container | Renders one child element per value in an array field (multi-reference, Option multi-select, etc.). |
data-algolia-repeat-item | child (inside repeat container) | Template element cloned for each array value. Its text content is set to each value in turn. |
Common patterns
Practical recipes for the most common implementation tasks.
The Webflow API returns field slugs (lowercase, hyphenated), not display names. Use these slugs in data-algolia-bind, data-algolia-filter, etc.
| Webflow Designer (display name) | API slug (Algolia field) |
|---|---|
| Product Name | name |
| Car Make | car-make |
| Featured? | featured |
The sync resolves single Reference fields to the referenced item's name automatically. It also stores all sub-fields as field__subfield. Multi-references become arrays.
<!-- Brand logo from the referenced "Car Brand" collection -->
<img data-algolia-bind="car-brand__logo.url" data-algolia-attr="src">
<!-- Brand name string -->
<p data-algolia-bind="car-brand"></p>Option fields are stored as the option's name (e.g. "White"), not its internal ID. Use the name in your filter values.
<label data-algolia-filter="color-theme" data-algolia-value="White">
<input type="checkbox"><span>White</span>
</label>Webflow "Switch" fields are stored as real booleans in Algolia (true / false). Use the string "true" or "false" as the filter value.
<label data-algolia-filter="featured" data-algolia-value="true">
<input type="checkbox">
<span>Featured only</span>
</label>Image fields are stored as objects with a url property. Use dot notation to bind the URL to a src attribute.
<img data-algolia-bind="image.url" data-algolia-attr="src">For arrays / multi-reference fields where each value should render as a separate element, use data-algolia-repeat. Use data-algolia-hide-empty to hide the wrapper when the field is empty.
<div data-algolia-hide-empty="authors" data-algolia-repeat="authors">
<span data-algolia-repeat-item class="author-tag"></span>
</div>Multi-reference fields (stored as arrays in Algolia) are fully filterable. Algolia indexes each value in the array individually, so a filter on one value matches any item where that value appears. Two steps: (1) add the field slug as a Facet in Algolia (index → Configuration → Facets), or filters silently return zero results; (2) add filter elements exactly as for any other field. The search input works too — add the field to Searchable Attributes and text search matches across all array values.
<!-- Checkbox: show items by any selected author -->
<label data-algolia-filter="authors" data-algolia-value="Charles Miller">
<input type="checkbox"><span>Charles Miller</span>
</label>
<label data-algolia-filter="authors" data-algolia-value="Emily Davis">
<input type="checkbox"><span>Emily Davis</span>
</label>
<!-- Or a dropdown -->
<select data-algolia-filter-select="authors">
<option value="">All authors</option>
<option value="Charles Miller">Charles Miller</option>
<option value="Emily Davis">Emily Davis</option>
</select>data-algolia-hide-empty hides the element when the bound field is empty, null, or an empty array — useful for optional fields in cards.
<div data-algolia-hide-empty="authors" data-algolia-repeat="authors">
<span data-algolia-repeat-item></span>
</div>Use data-algolia-filter-all for a radio "All" option (auto-activates when nothing is selected). Or add checked to a specific input to set a default. URL state always takes priority.
<!-- Default to "All" -->
<label data-algolia-filter-all="car-brand">
<input type="radio" name="brand">
<span>All brands</span>
</label>
<!-- Default to a specific value -->
<label data-algolia-filter="car-brand" data-algolia-value="Quasar">
<input type="radio" name="brand" checked>
<span>Quasar</span>
</label>Add data-algolia-url-state to the wrapper to sync filters, search, and page number to URL params. Users can copy the URL to share their filtered view.
<div data-algolia data-algolia-app-id="YOUR_APP_ID" data-algolia-api-key="YOUR_KEY" data-algolia-index="cars" data-algolia-url-state
>
<!-- filter elements -->
</div>Add data-algolia-scroll-anchor to any element above the results. The page scrolls to it on every filter, search, sort, or pagination change.
<div data-algolia-scroll-anchor></div>
<div data-algolia-list>
<!-- results -->
</div>Shared code, isolated infrastructure — one template repo can power any number of Webflow sites. Each project only needs its own Algolia index, Webflow Cloud app, and Cloudflare Worker with isolated env vars. Use the official template directly when the code works as-is. Fork (or "Use this template" to make your own copy) only if you need to: modify the sync logic, pin a different library version per project, keep an isolated commit history, or — importantly — use Feature B (federated search) with project-specific sources. Because search-all-config.ts is committed code (not env vars), each site with a different set of federated collections/pages needs its own repo copy.
# Per-project resources — always create fresh:
# - A dedicated Algolia index
# - A dedicated Webflow Cloud app (with its own env vars)
# - A dedicated Cloudflare Worker (with its own env vars)
# - Webflow webhooks pointing at that project's Cloudflare Worker URL
# Shared across projects (no duplication needed):
# - The client <script> tag (same jsDelivr URL)
# - The webhook worker code (apps/webhook-worker/src/index.js)
# - The sync app code (apps/sync) — Feature A only
# Feature B caveat:
# search-all-config.ts is committed code, so projects with
# DIFFERENT federated sources need their own copy of the repo.Built-in Inspector
The library ships with an Inspector that audits your page for configuration mistakes. It only loads on staging (*.webflow.io) and local development hosts, and only when you explicitly opt in.
Activate it
Append ?algolia-debug to your staging URL:
https://your-site.webflow.io/cars?algolia-debugYou will see a floating Algolia Inspector badge in the bottom-right corner. A red or yellow dot indicates issues; green means clean. All elements with data-algolia* attributes get a cyan outline and tooltip. Click the badge to open the diagnostic panel.
Controls
| Action | Result |
|---|---|
| ?algolia-debug in URL | Loads the Inspector for this page |
| Remove the param (or refresh without it) | Inspector unloads |
| Click the floating badge | Opens / closes the diagnostic panel |
| Toggle "Outline" checkbox in the panel | Shows / hides the cyan outlines + tooltips |
| Shift + ? keyboard shortcut | Same as toggling outline |
| Click any issue in the panel | Smoothly scrolls to the offending element and pulses an outline around it |
What it checks
Wrapper
[data-algolia] exists; required data-algolia-app-id, data-algolia-api-key, data-algolia-index present; data-algolia-match-mode is "and" or "or"
Templates
[data-algolia-list] and [data-algolia-template] both exist; template contains at least one data-algolia-bind; data-algolia-attr always paired with data-algolia-bind; data-algolia-bind / -hide-empty not left with empty values; data-algolia-repeat-item lives inside a data-algolia-repeat
Filters
data-algolia-filter paired with data-algolia-value; orphan data-algolia-value flagged; radio groups consistent — if any radio is wired up, every radio in that name group must be too; radios in the same filter share a name; non-empty values for -filter-select / -filter-all
Range
Every data-algolia-range-min="attr" has a matching data-algolia-range-max="attr" (and vice versa)
Range Slider
Has either static min/max OR auto-bounds; track + both handles present; matching number inputs in the wrapper
Pagination
Load More not combined with numbered Pages; [data-algolia-pages] has a button template; page templates live inside [data-algolia-pages]
Tags
[data-algolia-tags] has a [data-algolia-tag-template] child; tag children (-tag-label, -tag-remove) live inside the template
The Inspector never runs in production. Even with ?algolia-debug on a custom-domain site, nothing happens. Safe to ship.
Runbook
Common maintenance tasks after your initial deployment.
After adding new fields, new collections, or modifying references in Webflow, run a full sync of whichever feature(s) you use. Publishing the site also triggers this automatically via the site_publish webhook. Note: After editing search-all-config.ts, deploy the sync app first, then re-run /api/search-all.
# Feature A — filter index
curl -X POST https://YOUR_SITE.webflow.io/api/sync \
-H "Authorization: Bearer YOUR_SYNC_SECRET"
# Feature B — federated search index
curl -X POST https://YOUR_SITE.webflow.io/api/search-all \
-H "Authorization: Bearer YOUR_SYNC_SECRET"In Algolia → your index → Manage index → Clear index → type CLEAR. Then re-run the sync.
# After clearing in the Algolia dashboard, re-sync:
curl -X POST https://YOUR_SITE.webflow.io/api/sync \
-H "Authorization: Bearer YOUR_SYNC_SECRET"When a new version is released, update the version tag in the script URL. Then hard refresh (Cmd/Ctrl+Shift+R) to bypass the browser cache.
<script src="https://cdn.jsdelivr.net/gh/felixeallan/algolia-webflow-filter@v0.8.13/packages/library/dist/algolia-webflow.min.js"></script>Fetch a single record from your Algolia index to verify field names and values after a sync.
curl -s "https://YOUR_APP_ID-dsn.algolia.net/1/indexes/YOUR_INDEX?hitsPerPage=1" \
-H "X-Algolia-Application-Id: YOUR_APP_ID" \
-H "X-Algolia-API-Key: YOUR_SEARCH_KEY"Use the PUT endpoint to return the raw Webflow collection schema — useful for debugging field types and resolving reference issues.
curl -X PUT https://YOUR_SITE.webflow.io/api/sync \
-H "Authorization: Bearer YOUR_SYNC_SECRET"Common issues
| Symptom | Cause | Fix |
|---|---|---|
| Page shows wrong filter values, only ~100 items | Old version of @main cached by jsDelivr | Always pin to a @v0.x.x tag, not @main |
| New library changes not appearing | Browser cache | Hard refresh (Cmd/Ctrl+Shift+R) |
| Webhook URL rejected: "Invalid hostname" | Webflow blocks *.webflow.io webhooks | Use the Cloudflare Worker URL instead |
| Items from other collections appear in Algolia | Webhook fires for all collections | Set WEBFLOW_COLLECTION_ID in the Cloudflare Worker |
| Reference field shows an ID instead of name | Old sync, or new reference field | Re-run sync (or publish the site) |
| Search results show no image | Bound image.url instead of image in the federated index | In the search_all index, image is a plain URL string — use data-algolia-bind="image" (not "image.url") |
| Search index never updates on edits | SEARCH_ALL_ENDPOINT missing or pointing at wrong domain | Set it on the Cloudflare Worker to https://YOUR_SITE.webflow.io/api/search-all and re-deploy |
| New collection / field not appearing in search results | search-all-config.ts edited but sync app not redeployed | Deploy the sync app first, then re-run POST /api/search-all |
| Search results page is blank on load | data-algolia-search-mode="empty" is set | Expected behavior — results appear once the visitor types. Remove the attribute to show all records by default. |
| Webflow Cloud deploy fails (Cannot find package esbuild) | Webflow Cloud installs with --omit=dev | All build-time deps must be regular dependencies (already configured in the template) |
| Webflow Cloud deploy succeeds but routes 500 | Next.js 16.2+ Turbopack output crashes on Workers | Already pinned to ~16.1 with next build --webpack |
Project structure
Where each piece lives in the template repo — the sync endpoints for both features, the federated config file, the webhook worker, and the client library.
algolia-webflow-filter/
├── apps/
│ ├── sync/ # Next.js app for Webflow Cloud
│ │ └── src/
│ │ ├── search-all-config.ts # Feature B — EDIT THIS: collections + static pages
│ │ └── app/
│ │ ├── sync/route.ts # POST /api/sync — Feature A full paginated sync
│ │ │ # GET /api/sync — health check
│ │ │ # PUT /api/sync — schema dump (debug)
│ │ ├── search-all/route.ts # POST /api/search-all — Feature B federated sync
│ │ └── webhook/route.ts # POST /api/webhook (unused — use the Worker instead)
│ │
│ └── webhook-worker/ # Cloudflare Worker
│ └── src/index.js # Per-item sync + full sync on site_publish
│
└── packages/
└── library/ # Client-side library (filtering + federated search)
├── src/index.ts # Source
└── dist/algolia-webflow.min.js # Built (served via jsDelivr)Ready to build?
Clone the Webflow template, follow the 11 steps, and have Algolia-powered filtering live on your Webflow site today.