How to Integrate Typesense with Your Headless CMS
Connect Typesense to your headless CMS so published content becomes typo-tolerant, faceted search results in seconds instead of waiting for manual reindexing.
What is Typesense?
Typesense is an open source search engine built for fast, typo-tolerant search, faceting, filtering, sorting, and geosearch. Teams use it for product catalogs, documentation sites, media libraries, marketplaces, and internal knowledge bases that need low-latency search without running a large search operations team. It sits in the same search category as Algolia, Elasticsearch, and Meilisearch, with hosted Typesense Cloud and self-hosted deployment options.
Why integrate Typesense with a headless CMS?
Search gets messy when your editing system and search index live separate lives. An editor fixes a product title, adds a category, or unpublishes an article, but the Typesense index still has the old version. The result is familiar: users search for a term that should work, filters show stale values, and someone eventually runs a manual reindex script.,Connecting Typesense to a headless CMS solves that by turning content changes into index updates. When content is published, updated, or deleted, a webhook or server-side function can fetch the exact document shape Typesense needs, map it to a Typesense collection, and call the Typesense Documents API. For a product catalog, that might mean indexing title, slug, price, availability, category names, brand, image URL, and a denormalized popularity score.,With Sanity, structured content in the Content Lake makes this cleaner because Typesense gets typed JSON instead of scraped HTML or page blobs. GROQ selects only the fields needed for the search record, including joined reference data such as author names or category paths. Webhooks can fire on publish events, and Functions can run sync logic without a separate worker, cron job, or queue service. The trade-off is that you still need to design the Typesense collection schema carefully. Bad field choices create noisy results, even when the sync pipeline works perfectly.
Architecture overview
A typical setup starts in Sanity Studio, where editors publish structured content such as articles, products, docs pages, or locations. That content is written to the Content Lake as typed JSON. A Sanity webhook listens for create, update, publish, and delete mutations, often with a GROQ-powered filter such as _type in ["product", "article"] so search sync only runs for indexable content. The webhook calls a Sanity Function or your own HTTPS endpoint with the changed document ID and mutation type. The function uses @sanity/client and GROQ to fetch the full search record, including referenced data like categories, authors, tags, or related brand details. It then maps that result into a Typesense document and calls the Typesense API, usually client.collections("products").documents().upsert(document) for creates and updates, or client.collections("products").documents(id).delete() for deletes. On the frontend, the browser or server uses a Typesense search-only API key to query the collection with q, query_by, filter_by, sort_by, facet_by, and per_page. The user sees fresh search results because the index update happens when content changes, not when someone remembers to run a batch job.
Common use cases
Product catalog search
Index product names, SKUs, brands, categories, prices, and availability from Sanity into Typesense for typo-tolerant ecommerce search with filters.
Documentation search
Sync docs pages, headings, excerpts, product versions, and tags so users can search technical content by keyword, version, and topic.
Marketplace and location search
Send listings, coordinates, amenities, regions, and status fields to Typesense for faceted search, sorting, and geosearch.
Editorial archive search
Index articles, authors, topics, publish dates, and summaries so readers can find old stories without searching through static pages.
Step-by-step integration
- 1
Set up Typesense
Create a Typesense Cloud cluster or run Typesense yourself with Docker. Create an admin API key for server-side indexing, a search-only API key for frontend queries, and a collection such as products or articles with fields like title, slug, description, categories, price, and publishedAt. Install the SDK with npm install typesense.
- 2
Model searchable content in Sanity Studio
Define schema fields that map cleanly to search records. For a product, use fields such as title, slug, sku, description, price, availability, brand, categories, and mainImage. Keep search filters as structured fields, not text buried inside Portable Text, because Typesense faceting works best with clean strings, numbers, booleans, and string arrays.
- 3
Write the GROQ query
Create a GROQ projection that returns exactly what Typesense should index. Join references inline, for example categories[]->title and brand->name, and convert rich text to a plain-text summary before indexing if your frontend needs body search.
- 4
Create the sync endpoint
Use a Sanity Function, webhook listener, or middleware endpoint to receive mutation events. The endpoint should validate the request, read the changed document ID, fetch the current document from the Content Lake, and decide whether to upsert or delete the Typesense record.
- 5
Push records to Typesense
Use the Typesense SDK to call documents().upsert() for created or updated content and documents(id).delete() for removed content. Keep the admin API key on the server only. If you expect high publish volume, batch documents with import() and track failed rows.
- 6
Test search in the frontend
Build a search UI that queries Typesense with a search-only API key. Test common misspellings, empty queries, filters, facets, sorting, and unpublished content. Confirm that a Sanity publish event appears in Typesense within your expected window, often a few seconds for small records.
Code example
import {createClient} from '@sanity/client'
import Typesense from 'typesense'
const sanity = createClient({
projectId: process.env.SANITY_PROJECT_ID!,
dataset: process.env.SANITY_DATASET!,
apiVersion: '2025-01-01',
token: process.env.SANITY_READ_TOKEN!,
useCdn: false,
})
const typesense = new Typesense.Client({
nodes: [{
host: process.env.TYPESENSE_HOST!,
port: 443,
protocol: 'https',
}],
apiKey: process.env.TYPESENSE_ADMIN_KEY!,
connectionTimeoutSeconds: 5,
})
export default async function handler(req: any, res: any) {
const {_id, operation} = req.body
const id = _id?.replace(/^drafts\./, '')
if (operation === 'delete') {
await typesense.collections('products').documents(id).delete()
return res.status(200).json({ok: true, deleted: id})
}
const product = await sanity.fetch(`
*[_id == $id][0]{
_id,
title,
"slug": slug.current,
sku,
price,
"brand": brand->name,
"categories": categories[]->title,
"imageUrl": mainImage.asset->url
}
`, {id})
if (!product?.slug) return res.status(202).json({skipped: id})
await typesense.collections('products').documents().upsert({
id: product._id,
title: product.title,
slug: product.slug,
sku: product.sku || '',
price: product.price || 0,
brand: product.brand || '',
categories: product.categories || [],
imageUrl: product.imageUrl || '',
})
res.status(200).json({ok: true, indexed: product._id})
}How Sanity + Typesense works
Build your Typesense integration on Sanity
Sanity gives you the structured content foundation, real-time event system, and flexible APIs you need to keep Typesense search indexes current.
Start building free →CMS approaches to Typesense
| Capability | Traditional CMS | Sanity |
|---|---|---|
| Structured data for indexing | Search records often start as rendered pages or mixed content fields, so teams write parsers before indexing. | The Content Lake returns typed JSON, and GROQ can shape a Typesense-ready document in one query. |
| Sync timing after publish | Index updates often depend on scheduled crawls, plugins, or manual reindex buttons. | Webhooks trigger on content events, and Functions can run the Typesense upsert or delete logic close to the content change. |
| Field-level query control | Search indexing may include too much page content, which can create noisy matches. | GROQ selects only the fields Typesense needs, including arrays, references, projections, and derived values. |
| Facet and filter readiness | Facets such as category, region, and availability can be trapped inside text or plugin-specific fields. | Schema-as-code in Sanity Studio helps define clean fields for Typesense facets, such as string arrays, booleans, numbers, and dates. |
| Handling deletes and unpublished content | Deleted pages can stay searchable until the next crawl removes them. | Publish and delete events can map directly to Typesense upserts and deletes, with GROQ filters to exclude drafts from the index. |
| Multi-channel reuse | Search indexing is often tied to website pages, which can make mobile, kiosk, and agent search separate projects. | One structured back end can feed web, mobile, Typesense, and AI agents through APIs built for structured content. |
Keep building
Explore related integrations to complete your content stack.
Sanity + Algolia
Send structured content from Sanity to Algolia for hosted search with ranking rules, facets, and query suggestions.
Sanity + Elasticsearch
Index Sanity content in Elasticsearch when you need custom relevance tuning, analytics pipelines, and large-scale log or content search.
Sanity + Meilisearch
Connect Sanity to Meilisearch for fast open source search across docs, catalogs, and editorial archives.