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

A Webflow site with a CMS collection
A free Algolia account
A free Cloudflare account
A GitHub account
Shared Foundation

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 IDALGOLIA_APP_ID - Search API Key → used later in the script tag (safe to expose in the browser) - Write API KeyALGOLIA_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.
VariableType
WEBFLOW_API_TOKENSecret
ALGOLIA_APP_IDText
ALGOLIA_ADMIN_API_KEYSecret — this is the Write API Key
SYNC_SECRETSecret — 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.

  1. Go to dash.cloudflare.comWorkers & PagesCreateCreate WorkerHello World
  2. Name it (e.g. algolia-webflow-webhook) → Deploy
  3. Click "Edit code", delete everything, and paste the contents of apps/webhook-worker/src/index.jsDeploy
  4. Go to Settings → Variables and Secrets and add the base variables below. Feature A adds ALGOLIA_INDEX_NAME + WEBFLOW_COLLECTION_ID; Feature B adds SEARCH_ALL_ENDPOINT.
  5. 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.

VariableType
ALGOLIA_APP_IDText
ALGOLIA_ADMIN_API_KEYSecret — the Write API Key
SYNC_ENDPOINTText — https://YOUR_SITE.webflow.io/api/sync
SYNC_SECRETSecret — same as Webflow Cloud
EventPurpose
collection_item_createdNew items trigger a re-sync
collection_item_changedEdits trigger a re-sync
collection_item_deletedDeletes remove the item from the filter index
collection_item_unpublishedUnpublished items removed from the filter index
site_publishTriggers 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 tag (Webflow footer)
<script src="https://cdn.jsdelivr.net/gh/felixeallan/algolia-webflow-filter@v0.8.13/packages/library/dist/algolia-webflow.min.js"></script>
Feature A

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.

  1. In Webflow → CMS → click the collection you want to filter → Settings → copy the Collection ID.
  2. Decide an index name (e.g. products, blog-posts, cars).
  3. In your Webflow Cloud appEnvironment Variables, add the two vars below, then click "Deploy latest commit".
  4. In your Cloudflare WorkerSettings → 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}:

VariableType
WEBFLOW_COLLECTION_IDText — the collection you're filtering
ALGOLIA_INDEX_NAMEText — the index name you chose, e.g. "products"
Health check
curl https://YOUR_SITE.webflow.io/api/sync

Trigger the first full sync. Check Algolia → your index → Browse — all your CMS items should appear there.

Trigger sync
curl -X POST https://YOUR_SITE.webflow.io/api/sync \
  -H "Authorization: Bearer YOUR_SYNC_SECRET"
Expected response
{ "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 optionReplica name
Name A → Zcars_name_asc
Name Z → Acars_name_desc
Price ↑cars_price_asc
Price ↓cars_price_desc
Year newestcars_year_desc
Year oldestcars_year_asc
Sort dropdown
<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.

Full HTML example
<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>
Feature B

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.

VariableType
objectIDUnique ID, prefixed per source (car__<id>, author__<id>, page__about) so IDs never collide across collections
titleThe result's headline
descriptionSupporting text (optional per source)
urlAbsolute link to the page, built from SITE_URL + the item's slug
imageImage URL as a plain string (already extracted — bind "image", NOT "image.url")
typeThe source label (Car, Author, Page…) — used for the type filter
dateISO 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`).
search-all-config.ts
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".

VariableType
ALGOLIA_SEARCH_ALL_INDEXText — the federated index name, e.g. "search_all"
SITE_URLText — 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.

Trigger search sync
curl -X POST https://YOUR_SITE.webflow.io/api/search-all \
  -H "Authorization: Bearer YOUR_SYNC_SECRET"
Expected response
{ "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 optionReplica name
Newest firstsearch_all_date_desc
Oldest firstsearch_all_date_asc

Add one variable to your Cloudflare WorkerSettings → 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.
VariableType
SEARCH_ALL_ENDPOINTText — 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.

Navbar search box
<!-- 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>
Results page (/search)
<!-- 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.

Show attributes for:

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.

Clone Template
Required

Required Setup

The minimum needed for both filtering and search to work.

Wrapper

Both
AttributeOnDescription
data-algoliaRequired
any (wrapper div)Marks the root. Everything else must be inside.
data-algolia-app-id="..."Required
wrapperAlgolia Application ID
data-algolia-api-key="..."Required
wrapperAlgolia Search-Only API key (safe to expose)
data-algolia-index="..."Required
wrapperAlgolia index name
data-algolia-hits-per-page="12"
wrapperNumber 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
Every attribute you filter by must be added as a Facet in Algolia (your index → Configuration → Facets). Without this, filters silently return no results. Radio buttons in the same filter group must share the same name attribute so the browser treats them as mutually exclusive. Checkboxes do not need a shared name.
AttributeOnDescription
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
AttributeOnDescription
data-algolia-listRequired
containerWhere 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

Optional

Add any of these to extend your UI.

Pagination

Both
Load More and Pagination are independent options — pick one. Avoid combining them. You can combine Prev/Next with Page counter and/or Numbered pages. Algolia caps pagination at 1,000 total results by default. Increase paginationLimitedTo in Algolia → your index → Configuration → Pagination if you need more.

Load More

A single button that appends the next page of results to the existing list. Auto-hides when there are no more pages.

AttributeOnDescription
data-algolia-load-more
anyAppend 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.

AttributeOnDescription
data-algolia-prev
anyGo to previous page. Auto-disabled on the first page.
data-algolia-next
anyGo to next page. Auto-disabled on the last page.

Page counter

Displays "Page X of Y". Optional companion to Prev / Next.

AttributeOnDescription
data-algolia-page-info
anyShows 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.

AttributeOnDescription
data-algolia-pages
containerWhere numbered page buttons are injected.
data-algolia-page-button-template
child of pages containerCloned 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 containerPages 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 containerPages always shown at the start and end of the list. Same breakpoint syntax. Default 1.

Sort

Both
AttributeOnDescription
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
AttributeOnDescription
data-algolia-clear
anyClears ALL filters, search query, and sort selection.
data-algolia-clear="attr"
anyClears only the specified filter group (e.g. data-algolia-clear="car-brand").

Empty State

Both
AttributeOnDescription
data-algolia-empty
anyShown only when the current query returns zero results. Hidden otherwise.

Active Filter Tags

Both
AttributeOnDescription
data-algolia-tags
containerWhere active filter tags get injected.
data-algolia-tag-template
child of tags containerCloned once per active filter. Hidden until filters are active.
data-algolia-tag-label
any (inside tag template)Gets the active filter value as text (e.g. "Quasar", "White").
data-algolia-tag-remove
any (inside tag template)Clicking removes that filter. If omitted, the whole tag element is clickable.

Scroll Anchor

Both
AttributeOnDescription
data-algolia-scroll-anchor
anyScrolls 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
AttributeOnDescription
data-algolia-count
anyDisplays the total number of results matching the current query (e.g. 1,786). Updates on every filter change.
data-algolia-query
anyDisplays 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
The slider must coexist with data-algolia-range-min / data-algolia-range-max inputs for the same attribute somewhere in the wrapper. The slider drives those inputs — they are the source of truth. You can hide them with display:none if you only want the slider visible. Two-way sync is automatic: dragging a handle updates the number inputs; typing into the inputs moves the handles; clearing resets both. Auto-bounds fires one extra Algolia query on load and uses the real min/max from your data. Static bounds let you set clean round numbers but must be updated as your data grows.
AttributeOnDescription
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 divBinds the slider to the same attr as the number inputs.
data-algolia-range-slider-min="0"Required
slider wrapperStatic lower bound of the slider scale. Required unless using auto-bounds.
data-algolia-range-slider-max="100000"Required
slider wrapperStatic 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 wrapperThe 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 trackThe lower drag handle.
data-algolia-range-slider-handle="max"Required
child of trackThe upper drag handle.
data-algolia-range-slider-display="min"
anyLive text element showing the current lower value.
data-algolia-range-slider-display="max"
anyLive text element showing the current upper value.

Code examples

html
<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
AttributeOnDescription
[data-algolia-filter][data-active]
CSS selectorWhen 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 classFor Webflow's native checkbox/radio components, the library also toggles this class for visual styling compatibility.
css
/* 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;
}
Advanced

Nested List

Render array / multi-reference fields as repeated child elements inside a result card.

Nested List (Repeat)

Filtering
AttributeOnDescription
data-algolia-repeat="field"
containerRenders 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 Namename
Car Makecar-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.

html
<!-- 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.

html
<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.

html
<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.

html
<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.

html
<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.

html
<!-- 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.

html
<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.

html
<!-- 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.

html
<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.

html
<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.

bash
# 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:

url
https://your-site.webflow.io/cars?algolia-debug

You 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

ActionResult
?algolia-debug in URLLoads the Inspector for this page
Remove the param (or refresh without it)Inspector unloads
Click the floating badgeOpens / closes the diagnostic panel
Toggle "Outline" checkbox in the panelShows / hides the cyan outlines + tooltips
Shift + ? keyboard shortcutSame as toggling outline
Click any issue in the panelSmoothly 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.

bash
# 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.

bash
# 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.

html
<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.

bash
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.

bash
curl -X PUT https://YOUR_SITE.webflow.io/api/sync \
  -H "Authorization: Bearer YOUR_SYNC_SECRET"

Common issues

SymptomCauseFix
Page shows wrong filter values, only ~100 itemsOld version of @main cached by jsDelivrAlways pin to a @v0.x.x tag, not @main
New library changes not appearingBrowser cacheHard refresh (Cmd/Ctrl+Shift+R)
Webhook URL rejected: "Invalid hostname"Webflow blocks *.webflow.io webhooksUse the Cloudflare Worker URL instead
Items from other collections appear in AlgoliaWebhook fires for all collectionsSet WEBFLOW_COLLECTION_ID in the Cloudflare Worker
Reference field shows an ID instead of nameOld sync, or new reference fieldRe-run sync (or publish the site)
Search results show no imageBound image.url instead of image in the federated indexIn the search_all index, image is a plain URL string — use data-algolia-bind="image" (not "image.url")
Search index never updates on editsSEARCH_ALL_ENDPOINT missing or pointing at wrong domainSet it on the Cloudflare Worker to https://YOUR_SITE.webflow.io/api/search-all and re-deploy
New collection / field not appearing in search resultssearch-all-config.ts edited but sync app not redeployedDeploy the sync app first, then re-run POST /api/search-all
Search results page is blank on loaddata-algolia-search-mode="empty" is setExpected 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=devAll build-time deps must be regular dependencies (already configured in the template)
Webflow Cloud deploy succeeds but routes 500Next.js 16.2+ Turbopack output crashes on WorkersAlready 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.

repo
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.