Integrations

Docusaurus

Make ZBSearch the default search engine of your Docusaurus site.

@zbsearch/plugin-docusaurus indexes your docs, blog posts and MDX pages while Docusaurus builds, and replaces the default search bar with a search dialog powered by ZBSearch.

The index ships with your static site, so search keeps working on a CDN, behind a VPN, or offline. No account, no API key, no query leaving the browser.

Installation

npm install @zbsearch/plugin-docusaurus

Then register it in docusaurus.config.ts:

export default {
  plugins: ['@zbsearch/plugin-docusaurus'],
};

That is the whole setup. Docusaurus renders @theme/SearchBar in the navbar of every page, and the plugin supplies it. Start the site and press ⌘K.

If your navbar already contains a { type: 'search' } item, the search box is rendered there instead. Both placements work.

What gets indexed

Every page is split into one record per heading, so a result lands on the exact section that matched rather than the top of a long page.

A record carries four searchable fields:

FieldContents
titleTitle of the page
sectionThe heading the chunk was taken from
hierarchyFolders the page lives in, its title, and the enclosing headings
contentThe prose beneath that heading

Front matter, fenced code, JSX and MDX imports are stripped before indexing: code blocks match poorly and drown prose in punctuation. Permalinks and category labels travel with each record but are deliberately not tokenized, so a query never matches a URL fragment.

Headings keep the anchors Docusaurus generates for them, including explicit ones:

## Hybrid search {#hybrid}

Options

import type { ZBSearchDocusaurusOptions } from '@zbsearch/plugin-docusaurus';

const options: ZBSearchDocusaurusOptions = {
  excludeRoutes: ['/docs/internal/**'],
  maxResults: 12,
};

export default {
  plugins: [['@zbsearch/plugin-docusaurus', options]],
};

Content

OptionDefaultDescription
language'english'Language used to tokenize and stem the index
docstrueIndex every docs plugin instance
blogtrueIndex every blog plugin instance
pagestrueIndex standalone MDX pages
indexAllDocsVersionsfalseIndex older docs versions as well as the current one
excludeRoutes[]Routes to leave out, with * and ** wildcards
categoryLabels{ docs: 'Docs', blog: 'Blog', pages: 'Pages' }Labels used to tag results

Only the current docs version is indexed by default. Older versions of the same page are near-identical and would push genuinely different pages out of the result list.

React pages written as .tsx are skipped: running a markdown parser over JSX yields prop names, not prose.

Ranking

OptionDefaultDescription
maxResults12Maximum number of hits shown at once
boost{ title: 4, section: 3, hierarchy: 1.5, content: 1 }Per-property ranking weights
tolerance1Edit distance tolerated per term
threshold0Minimum share of query terms a document must match
snippetLength140Maximum length of the excerpt under a hit

Raising tolerance makes the search forgiving of typos at the cost of precision. Raising threshold requires results to match more of the query, which narrows a broad search.

Interface

OptionDefaultDescription
recentSearchestrueRemember and replay recently opened results
hotkeystrueBind the ⌘K / Ctrl+K and / shortcuts
searchButtonLabel'Search'Text of the navbar button
placeholder'Search documentation…'Placeholder of the search input
labels{}Copy overrides for the dialog

Every string in the dialog can be replaced through labels, which is how you localise it:

const options = {
  searchButtonLabel: 'Cerca',
  labels: {
    placeholder: 'Cerca nella documentazione…',
    noResultsHint: 'Prova un altro termine.',
    selectHint: 'per aprire',
    navigateHint: 'per navigare',
    closeHint: 'per chiudere',
  },
};

How it works

Indexing happens in allContentLoaded, the one Docusaurus hook that behaves identically in docusaurus start and docusaurus build. The dev server therefore searches the same index the production site ships, with no second code path to keep in sync.

The finished index is serialized with ZBSearch's save() and written to .docusaurus/zbsearch-index/. The search bar imports it through a dynamic import(), so webpack emits it as its own chunk: the index - and ZBSearch itself - are only downloaded once a visitor shows intent to search, and never weigh on the first page load.

Changing a page in dev regenerates the index on reload. If the index ever looks stale, npm run clear removes .docusaurus and forces a fresh one.

Styling

Every colour, radius and font in the search box is a --zbs-* custom property, so you can re-theme it from src/css/custom.css without overriding a single rule:

:root {
  --zbs-accent: #0aa;
  --zbs-radius: 8px;
  --zbs-surface: #fff;
}

[data-theme='dark'] {
  --zbs-accent: #5ee;
  --zbs-surface: #16121d;
}

The dialog follows your site's theme toggle rather than the operating system: it reads the data-theme attribute Docusaurus sets on <html>, and only falls back to prefers-color-scheme when a host declares no theme at all.

The full list of properties lives in the search box documentation.

Keyboard

KeysAction
⌘K, Ctrl+K, /Open the search box
Move through the results, wrapping at both ends
Home EndJump to the first or last result
EnterOpen the selected result
-clickOpen a result in a new tab
EscClose

Troubleshooting

The navbar shows no search button. The plugin has to come after @docusaurus/preset-classic, which is the case whenever it sits in plugins rather than inside a preset. Check that it is registered in docusaurus.config.ts and restart the dev server.

Search finds nothing. The plugin logs no content was indexed during the build when every content plugin came back empty. Check that docs, blog or pages is enabled, and that excludeRoutes is not broader than you meant.

A page is missing from the results. Pages with no prose at all produce no records. React pages are skipped by design; convert one to .md or .mdx if it should be searchable.

On this page