How to Integrate Meilisearch with Your Headless CMS
Connect Meilisearch to your headless CMS so every publish, update, and delete can become typo-tolerant search results without manual reindexing.
What is Meilisearch?
Meilisearch is an open-source search engine built for fast, typo-tolerant search across sites, apps, documentation, catalogs, and internal tools. It supports prefix search, faceting, filtering, sorting, synonyms, ranking rules, and SDKs for JavaScript, Python, PHP, Ruby, Go, Rust, and more. Teams often choose Meilisearch when they want a simpler API and lighter operational footprint than running a larger search cluster.
Why integrate Meilisearch with a headless CMS?
Search gets stale fast when your content system and search index don't talk to each other. A product editor changes a title, a docs writer unpublishes an outdated article, or a marketing team adds a new landing page, but users still see the old result until someone runs a script or waits for a nightly batch job.
Connecting Meilisearch to a headless CMS solves that timing problem. When content is published, updated, or deleted, a webhook or server-side function can fetch the exact fields Meilisearch needs, format the document, and call Meilisearch's indexing API. For a search page, that means fresher results, better filters, and fewer support tickets that start with, "I can't find the new page."
This works best when the source content is structured. With Sanity's AI Content Operating System, content in the Content Lake is typed JSON, so you can send Meilisearch clean fields like title, slug, category, excerpt, image alt text, and publish date. The alternative is scraping rendered HTML or pushing large blobs into the index, which makes ranking, filtering, and result design harder than it needs to be.
Architecture overview
A typical Sanity and Meilisearch integration starts with content in the Content Lake. Editors work in Sanity Studio, and when an article, product, location, or help document is published, updated, or deleted, a GROQ-powered webhook fires for the matching document type. That event can call a Sanity Function, a Next.js route handler, or another small webhook listener. The sync layer receives the mutation payload, uses @sanity/client to fetch the published document with a GROQ query, and projects only the fields Meilisearch should index. For example, the query can join references so a product record includes category names, brand names, and collection slugs instead of internal reference IDs. Then the sync layer calls Meilisearch through its SDK, usually with index.addDocuments() for creates and updates, or index.deleteDocument() when the published document no longer exists. From there, your frontend queries Meilisearch directly with a public search key. The browser or server sends a search request to an index such as articles, products, or locations, passes query text plus filters like category = "guides" or inStock = true, and renders highlighted results. Sanity remains the structured source for content, while Meilisearch handles ranking, typo tolerance, facets, and fast result retrieval.
Common use cases
Documentation search
Index titles, headings, excerpts, tags, and product versions so users can find the right guide even with typos or partial terms.
Product discovery
Sync product names, categories, prices, availability flags, and attributes into Meilisearch for faceted catalog search.
Location finder
Send store names, services, regions, and coordinates to Meilisearch so visitors can filter by state, service type, or opening status.
Editorial archive search
Make thousands of articles searchable by title, author, topic, date, and related entities without indexing full page markup.
Step-by-step integration
- 1
Set up Meilisearch
Create a Meilisearch Cloud project or run Meilisearch yourself, then copy the host URL and admin API key. Install the JavaScript clients with npm install meilisearch @sanity/client, and create an index such as articles or products.
- 2
Configure the Meilisearch index
Use the Meilisearch SDK to set searchableAttributes, filterableAttributes, sortableAttributes, and rankingRules. For example, searchableAttributes might include title, excerpt, bodyText, category, and tags, while filterableAttributes might include category, locale, and publishedAt.
- 3
Model searchable content in Sanity Studio
Define schema fields that map cleanly to search results, such as title, slug, excerpt, category reference, image alt text, locale, tags, and publishedAt. Keep display fields separate from long Portable Text fields so you can choose what gets indexed.
- 4
Create the sync trigger
Add a GROQ-powered webhook for the document types you want to index, such as article, product, or location. Trigger only on publish, update, and delete events, and send the document _id to a Sanity Function or webhook route.
- 5
Fetch, transform, and index
In the sync handler, fetch the published document from the Content Lake with GROQ, flatten references into search-friendly fields, and call Meilisearch's addDocuments() or deleteDocument() API.
- 6
Build and test the search UI
Use instant-meilisearch, the Meilisearch JavaScript SDK, or your own API route to query the index. Test typos, empty queries, filters, deleted documents, locale handling, and permission boundaries before shipping.
Code example
import {createClient} from '@sanity/client'
import {MeiliSearch} from 'meilisearch'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
useCdn: false,
token: process.env.SANITY_READ_TOKEN
})
const meili = new MeiliSearch({
host: process.env.MEILI_HOST!,
apiKey: process.env.MEILI_ADMIN_KEY!
})
const query = `*[_id == $id && !(_id in path("drafts.**"))][0]{
"id": _id,
title,
"slug": slug.current,
excerpt,
"category": category->title,
"publishedAt": coalesce(publishedAt, _createdAt)
}`
export async function POST(req: Request) {
const body = await req.json()
const id = String(body._id || '').replace(/^drafts\./, '')
const doc = await sanity.fetch(query, {id})
const index = meili.index('articles')
if (!doc) {
await index.deleteDocument(id)
return Response.json({deleted: id})
}
await index.addDocuments([doc], {primaryKey: 'id'})
return Response.json({indexed: doc.id})
}How Sanity + Meilisearch works
Build your Meilisearch integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs you need to keep Meilisearch in sync with every content change.
Start building free →CMS approaches to Meilisearch
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Structured data for indexing | Often starts from rendered pages or plugin-specific fields, which can push noisy HTML into the search index. | The AI Content Operating System keeps content as typed JSON in the Content Lake, so Meilisearch can receive clean, index-ready documents. |
| Real-time sync on publish | Search updates often depend on plugins, scheduled jobs, or manual reindex buttons. | Webhooks and Functions can trigger on content mutations, run server-side sync logic, and call Meilisearch without polling. |
| Field-level query control | Search payloads are often tied to page templates, which makes it harder to exclude navigation, embeds, or layout text. | GROQ can project exactly the fields Meilisearch needs, including joined reference data, in a single query. |
| Delete and unpublish handling | Deleted pages can remain in search until a full reindex runs or a plugin catches the change. | Use the Sanity document _id as the Meilisearch primary key, then call deleteDocument() when the published record no longer exists. |
| Search result design | Result cards may be limited to whatever the search plugin extracts from the rendered page. | Sanity Studio schemas can include search-specific fields like excerpt, image alt text, tags, and synonyms, while editors work in a custom React interface. |
| Operational trade-offs | Lowest initial setup if a plugin already fits, but harder to customize ranking data and sync behavior. | More control over schema, GROQ, and sync logic, but you should plan ownership for index settings, API keys, and webhook failure retries. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Algolia
Use Algolia when you need hosted search with advanced relevance controls, personalization options, and broad frontend tooling.
Sanity + Typesense
Connect Typesense to Sanity for typo-tolerant, faceted search with a simple API and self-hosting options.
Sanity + Elasticsearch
Send structured Sanity content to Elasticsearch for larger search workloads, analytics-style queries, and custom ranking pipelines.