Strapi, Sanity & Contentful Search in Minutes
Real-time indexing for headless CMS. Search your content instantly from any frontend, with zero database load and AI-powered relevance.
Decoupled architectures give you flexibility but leave search as an afterthought. Building reliable, fast search across multiple frontends is harder than it needs to be.
Headless CMSs separate content from presentation. Searching across multiple frontend frameworks, databases, and services requires custom solutions that are complex and fragile.
Keeping search indexes synchronized with your CMS is a nightmare. Webhooks fail, cron jobs miss updates, and you're left with stale search results and frustrated users.
Most developers focus on API performance and overlook search UX. Default search is slow, inflexible, and doesn't handle typos or synonyms — driving users away.
Real-time indexing. Multiple frontends. One powerful search API. No complexity.
Connect Strapi, Sanity, or Contentful with a lifecycle hook or webhook and a short endpoint of your own that calls the REST API. No package to install, no search servers to run.
Whether you serve React frontends, Vue apps, or static Jamstack sites — the same search API powers them all. No framework coupling.
Webhooks fire the moment content publishes. New articles, product updates, and changes appear in search within seconds.
Filter by content type, tags, locale, status, or custom fields. Build faceted search without writing backend logic (filter attributes must be marked filterable).
Users type "strapi serch" and still find "Strapi search guide". AI-powered ranking finds what users mean, not just what they type.
Search runs externally. Your CMS database stays focused on reads and writes, not expensive search queries.
Add a Strapi lifecycle hook (your own code, no plugin needed) that sends each created or updated entry to the bulk indexing endpoint.
// src/api/article/content-types/article/lifecycles.js
module.exports = {
async afterCreate(event) { await indexArticle(event.result); },
async afterUpdate(event) { await indexArticle(event.result); },
};
async function indexArticle(article) {
await fetch('https://app.searchjetengine.com/api/v1/index/bulk', {
method: 'POST',
headers: {
// Private key: server-side env var only
'Authorization': 'Bearer ' + process.env.SEARCHJET_PRIVATE_KEY,
'Content-Type': 'application/json',
},
body: JSON.stringify({
documents: [{
id: 'article-' + article.id,
title: article.title,
content: article.body,
url: 'https://example.com/blog/' + article.slug,
type: 'article',
date: article.publishedAt,
}],
}),
});
}Create a Sanity webhook that calls your own endpoint. The endpoint maps the document and sends it to SearchJet (see the route handler below).
// Sanity webhook settings (sanity.io/manage → API → Webhooks)
{
"name": "searchjet-sync",
"url": "https://your-site.com/api/searchjet-sync",
"trigger on": ["create", "update"],
"filter": "_type in ['post', 'product', 'page']",
"projection": "{ _id, _type, _updatedAt, title, 'slug': slug.current, 'body': pt::text(body) }",
"headers": { "x-webhook-secret": "YOUR_OWN_SECRET" }
}Point a Contentful webhook at your own endpoint (for example a Next.js route handler or serverless function) that indexes the published entry.
// Contentful webhook (Settings → Webhooks)
{
"name": "SearchJet Indexing",
"url": "https://your-site.com/api/contentful-sync",
"topics": [
"Entry.publish"
],
"headers": [
{ "key": "x-webhook-secret", "value": "YOUR_OWN_SECRET" }
]
}// Frontend search over your Strapi content (React)
import { useState } from 'react';
// Use a SEARCH key in the browser, never the private key
const SEARCH_KEY = process.env.NEXT_PUBLIC_SEARCHJET_SEARCH_KEY;
export default function BlogSearch() {
const [results, setResults] = useState([]);
async function search(q) {
if (!q) return setResults([]);
const params = new URLSearchParams({
q,
limit: '10',
// "type" must be marked filterable in your index settings
filters: 'type = "article"'
});
const res = await fetch(
'https://app.searchjetengine.com/api/v1/search?' + params,
{ headers: { Authorization: 'Bearer ' + SEARCH_KEY } }
);
const data = await res.json();
setResults(data.results); // results is the array of hits
}
return (
<div>
<input type="search" placeholder="Search the blog..."
onChange={e => search(e.target.value)} />
<div className="results">
{results.map(post => (
<article key={post.id}>
<h3><a href={post.url}>{post.title}</a></h3>
<p>{post.excerpt}</p>
<small>{post.date}</small>
</article>
))}
</div>
</div>
);
}// app/api/searchjet-sync/route.js
// Your own endpoint (Next.js), called by the Sanity webhook
export async function POST(req) {
if (req.headers.get('x-webhook-secret') !== process.env.WEBHOOK_SECRET) {
return new Response('Unauthorized', { status: 401 });
}
const doc = await req.json(); // shaped by the webhook projection
const res = await fetch('https://app.searchjetengine.com/api/v1/index/bulk', {
method: 'POST',
headers: {
'Authorization': 'Bearer ' + process.env.SEARCHJET_PRIVATE_KEY,
'Content-Type': 'application/json'
},
body: JSON.stringify({
documents: [{
id: doc._id,
title: doc.title,
content: doc.body,
url: 'https://example.com/' + doc.slug,
type: doc._type,
date: doc._updatedAt
}]
})
});
return Response.json(await res.json()); // includes taskUid
}// Map a Contentful "Entry.publish" webhook payload
// inside your own endpoint (same pattern as the Sanity route)
function toSearchJetDocument(entry, locale = 'en-US') {
const f = entry.fields;
return {
id: entry.sys.id,
title: f.title?.[locale],
content: f.body?.[locale], // assumes a Long text field
excerpt: f.excerpt?.[locale],
url: 'https://example.com/blog/' + f.slug?.[locale],
type: entry.sys.contentType.sys.id,
date: entry.sys.updatedAt,
metadata: { tags: f.tags?.[locale] || [] }
};
}
// Then POST { documents: [toSearchJetDocument(entry)] }
// to https://app.searchjetengine.com/api/v1/index/bulk
// with Authorization: Bearer <private key from an env var>Illustrative example of a headless CMS moving search to SearchJet. Your results will vary.
| Metric | Before | After | Impact |
|---|---|---|---|
| Content Indexing Time | Batch or cron syncs | Seconds after publish | Much faster |
| Search Query Speed | Slower CMS API queries | <150ms | Noticeably faster |
| API Performance | Baseline | Search off your CMS API | More headroom |
| Infrastructure Costs | Search servers needed | Included | No search servers to run |
Everything you need to know about SearchJet for headless CMS
Get started free. No credit card required. Real-time indexing for Strapi, Sanity, and Contentful.
Questions? Contact our team