Skip to main content

Implement @cmfcmf/docusaurus-search-local + Orama

Overview

You cannot directly "combine" @cmfcmf/docusaurus-search-local and Orama in a supported way today; instead, the idiomatic approach is to pick one: keep @cmfcmf/docusaurus-search-local (Lunr-based, fully local) or replace it with the official Orama Docusaurus plugin (@orama/plugin-docusaurus or @orama/plugin-docusaurus-v3).

Two Viable Architectures

Use @cmfcmf/docusaurus-search-local alone for offline, firewall-only, fully static search with Lunr.js and an Algolia Autocomplete-based UI.

Use Orama's Docusaurus plugin, which generates an Orama index during build and wires it into Orama's Searchbox UI, optionally backed by Orama Cloud.

Key Insight: If you want Orama, the clean solution is Option B; trying to bolt Orama onto the cmfcmf plugin means re‑implementing its frontend and index consumption layer.


Option A: Implement @cmfcmf/docusaurus-search-local

Installation

npm install @cmfcmf/docusaurus-search-local
# or
yarn add @cmfcmf/docusaurus-search-local

Configuration in docusaurus.config.js (Docusaurus v2)

// docusaurus.config.js
module.exports = {
// ...
plugins: [
[
require.resolve('@cmfcmf/docusaurus-search-local'),
{
indexDocs: true,
docsRouteBasePath: '/docs',
indexBlog: true,
blogRouteBasePath: '/blog',
indexPages: false,
language: ['en'], // or ['en', 'pt'] etc.
indexDocSidebarParentCategories: 1,
style: undefined,
lunr: {
tokenizerSeparator: /[\s\-]+/,
},
},
],
],
};

This generates a Lunr index at build time and wires the search bar automatically into the theme; it only works on the static build, not during yarn start.

When to Prefer This Option

  • Air‑gapped or highly regulated deployments where adding Orama's runtime is not desired
  • You're okay with Lunr and just need a functioning local search today

Extension Prompt

"Given a Docusaurus v2 site with @cmfcmf/docusaurus-search-local, show me how to extend the plugin's indexer to include custom front‑matter fields (e.g. tags, domain, product) in the search index, with concrete code examples."


Option B: Replace with Orama's Docusaurus Plugin

Pick the Right Orama Plugin

  • Docusaurus v2: @orama/plugin-docusaurus
  • Docusaurus v3: @orama/plugin-docusaurus-v3

Installation (Example for v2)

npm install @orama/plugin-docusaurus
# or
yarn add @orama/plugin-docusaurus

Configure in docusaurus.config.js

The Orama plugin typically:

  • Hooks into the build, walks docs/blog/pages
  • Builds an Orama index (local JSON or Orama Cloud)
  • Injects Orama Searchbox components into your theme (navbar search, etc.)
// docusaurus.config.js
module.exports = {
// ...
plugins: [
[
'@orama/plugin-docusaurus',
{
// example options – adapt to docs
indexBlog: true,
indexDocs: true,
indexPages: false,
docsRouteBasePath: '/docs',
blogRouteBasePath: '/blog',
orama: {
// local-only index:
mode: 'local',
// or Orama Cloud:
// mode: 'cloud',
// apiKey: process.env.ORAMA_API_KEY,
// indexId: 'your-index-id',
},
searchbox: {
placeholder: 'Search docs…',
showSearchButton: true,
},
},
],
],
};

Remove the Old Local Search Plugin

Delete @cmfcmf/docusaurus-search-local from plugins and uninstall the package to avoid conflicting multiple search UIs.

npm uninstall @cmfcmf/docusaurus-search-local

Extension Prompt

"Given a Docusaurus v3 site using @orama/plugin-docusaurus-v3 in local mode (no cloud), show a full example of customizing the Orama Searchbox UI (keyboard shortcuts, result templates, filtering by docs version) with all TypeScript types."


Why Not Combine Both?

Architectural Incompatibility

  • @cmfcmf/docusaurus-search-local is tightly coupled to Lunr's index format and Algolia Autocomplete's UI; its build pipeline produces a Lunr JSON and its React components expect that structure
  • Orama's plugin produces an Orama database and wires React components that know how to query Orama (local or cloud)

Implementation Requirements

To "use Orama with @cmfcmf/docusaurus-search-local" you would need to:

  • Disable its Lunr index and still hijack its UI, or
  • Ignore its UI, generate a parallel Orama index and mount your own searchbox in the navbar

Reality Check: At that point you have effectively implemented Option B and no longer need the cmfcmf plugin.

Next Steps

If you share your Docusaurus major version (2 vs 3) and requirements (offline only vs Orama Cloud OK, multi‑locale, versioned docs), a next step can be a concrete docusaurus.config + custom searchbox component tailored to your stack.


Additional Docusaurus Search Options

Yes, Docusaurus supports several search options beyond @cmfcmf/docusaurus-search-local and Orama.

Official / First‑Class Options

Algolia DocSearch (Official)

  • Built‑in support via themeConfig.algolia in the classic preset
  • Free for open documentation; uses Algolia's hosted crawler and index

Configuration Prompt:

"Configure Docusaurus vX with Algolia DocSearch, including contextual search, version filters, and language facets, with a full docusaurus.config example."

Typesense DocSearch

  • Community-maintained but documented on the official site
  • Uses a Typesense cluster (self‑hosted or managed) plus docusaurus-theme-search-typesense and a scraper

Local / Client‑Side Search Plugins

Lunr/FlexSearch-Based Options

  • Docusaurus docs list "Local Search" options as community plugins (e.g., Lunr-based) that build a static index at build time and run entirely client-side
  • These avoid external services and are suitable for air‑gapped or privacy‑sensitive deployments

Comparison Prompt:

"Show a comparison of Docusaurus local search plugins (Lunr/FlexSearch/others) with bundle size, indexing features, and multilingual support, including links to each plugin's repo."

Bring‑Your‑Own Search Backend

Custom SearchBar + External Engine

Docusaurus allows "Your own SearchBar component", meaning you can swizzle the search bar and integrate any backend (Meilisearch, Elasticsearch, Postgres-based, AI RAG, etc.).

This is the route most third‑party or AI search integrations take (e.g., DocuScout's guide to embedding an AI+search widget in a swizzled SearchBar).

Custom Integration Prompt:

"Design a custom Docusaurus search integration using a self-hosted Meilisearch cluster, including the crawler/indexer pipeline, SearchBar swizzle, and TypeScript React search UI."


Custom SearchBar Implementation Guide

Docusaurus exposes the navbar SearchBar as a theme component that you can "swizzle", letting you replace its implementation and wire it to any backend (Meilisearch, Elasticsearch, Postgres, RAG, etc.).

Swizzle Mechanism

1. Eject the SearchBar Component

npm run swizzle @docusaurus/theme-classic SearchBar -- --eject
# or
yarn swizzle @docusaurus/theme-classic SearchBar -- --eject

This creates src/theme/SearchBar/index.tsx (or .js / .jsx), which Docusaurus loads instead of the default theme component.

The swizzled component receives:

  • className?: string (for styling integration)
  • Standard React props

You can:

  • Replace the entire UI
  • Call any search backend API
  • Implement custom ranking, filtering, highlighting
  • Add keyboard shortcuts, autocomplete, etc.

SearchBar Best Practices

1. Keep the Navbar Contract Small

  • Implement only what the navbar expects: a search input and any popover/modal you want
  • Accept className and ...props and forward them to your root element so the navbar styling continues to work

2. Avoid Heavy Work on Every Keypress

  • Use debouncing or throttling when calling your backend (e.g., trigger search 200–300 ms after typing stops)
  • For large results, paginate or limit results client‑side to reduce render cost

3. Handle Empty and Error States Clearly

  • Show distinct UI for: "type to search", "no results", "error loading", and "loading…"
  • Keyboard accessibility: support Esc to close, arrow keys to move between results, Enter to select

4. Keep Config Out of the Component

  • Use environment variables or a config module for URLs, API keys, and index names
  • The SearchBar should only know how to call a generic search function, not how Meilisearch/Elasticsearch is hosted

5. Respect Docusaurus Routing

  • Use @docusaurus/Link (or useHistory in older setups) so navigation stays client‑side and preserves SPA behavior

Assume you have a Meilisearch index docs populated with { title, url, content, ... }.

1. Install Meilisearch Client

npm install meilisearch
# or
yarn add meilisearch

2. Create a Search Client Helper

// src/search/meilisearchClient.ts
import { Meilisearch } from 'meilisearch';

const client = new Meilisearch({
host: process.env.MEILI_HOST!,
apiKey: process.env.MEILI_API_KEY!, // or omit for public
});

const index = client.index('docs');

export async function searchDocs(query: string) {
if (!query.trim()) return [];
const { hits } = await index.search(query, {
limit: 10,
});
return hits as Array<{ title: string; url: string; snippet?: string }>;
}
// src/theme/SearchBar/index.tsx
import React, { useState, useEffect, useCallback } from 'react';
import Link from '@docusaurus/Link';
import { searchDocs } from '../../search/meilisearchClient';

const DEBOUNCE_MS = 250;

export default function SearchBar({ className }: { className?: string }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState<
Array<{ title: string; url: string; snippet?: string }>
>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);

const doSearch = useCallback(
(q: string) => {
if (!q.trim()) {
setResults([]);
setLoading(false);
setError(null);
return;
}
setLoading(true);
setError(null);
searchDocs(q)
.then(setResults)
.catch((e) => setError(e.message ?? 'Search failed'))
.finally(() => setLoading(false));
},
[]
);

useEffect(() => {
const handle = setTimeout(() => doSearch(query), DEBOUNCE_MS);
return () => clearTimeout(handle);
}, [query, doSearch]);

return (
<div className={className}>
<input
type="search"
placeholder="Search docs…"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
{query && (
<div className="searchResults">
{loading && <div>Searching…</div>}
{error && <div>Error: {error}</div>}
{!loading && !error && results.length === 0 && (
<div>No results</div>
)}
<ul>
{results.map((r) => (
<li key={r.url}>
<Link to={r.url}>{r.title}</Link>
{r.snippet && <div className="snippet">{r.snippet}</div>}
</li>
))}
</ul>
</div>
)}
</div>
);
}

4. Indexing Pipeline (Out of Scope of Docusaurus)

  • Use Meilisearch's crawler, a CI job, or a static‑site crawler to index Docusaurus pages into the docs index
  • For a quick start, generate a sitemap or use Docusaurus' built docs build as input to your own indexing script

Indexing Job Prompt:

"Design a Meilisearch indexing job for a Docusaurus site that runs in CI on every deploy, including URL discovery, content extraction from built HTML, and Meilisearch schema configuration."


Assume you have an Elasticsearch index docs with { title, url, content, ... }.

1. Install Elasticsearch Client

npm install @elastic/elasticsearch
# or
yarn add @elastic/elasticsearch

2. Create a Search Helper

// src/search/elasticsearchClient.ts
import { Client } from '@elastic/elasticsearch';

const client = new Client({
node: process.env.ES_NODE!,
auth: process.env.ES_USERNAME
? {
username: process.env.ES_USERNAME!,
password: process.env.ES_PASSWORD!,
}
: undefined,
});

export async function searchDocs(query: string) {
if (!query.trim()) return [];
const res = await client.search({
index: 'docs',
size: 10,
query: {
multi_match: {
query,
fields: ['title^3', 'content'],
},
},
highlight: {
fields: {
content: {},
},
},
});

const hits = (res.hits.hits ?? []) as any[];
return hits.map((h) => ({
title: h._source.title as string,
url: h._source.url as string,
snippet:
h.highlight?.content?.[0] ??
(h._source.content as string)?.slice(0, 160),
}));
}

3. Reuse the Same SearchBar UI

In src/theme/SearchBar/index.tsx, swap searchDocs import to the Elasticsearch helper and keep all UI logic identical.

This makes it trivial to switch between Meilisearch and Elasticsearch by changing one import.

Production SearchBar Prompt:

"Provide a production-grade Docusaurus SearchBar implementation backed by Elasticsearch, including keyboard navigation of results, version filtering using Docusaurus metadata, and security considerations for exposing the search endpoint."


Summary

Docusaurus provides maximum flexibility for search implementation:

  1. Official plugins (Algolia, Typesense) for managed solutions
  2. Local plugins (Lunr, Orama) for static/offline search
  3. Custom SearchBar for any backend (Meilisearch, Elasticsearch, RAG)

The swizzle mechanism enables complete control over search UX while maintaining Docusaurus theme integration.

For specific implementation guidance, share your Docusaurus version (2 vs 3) and preferred backend (Meilisearch, Elasticsearch, etc.).