---
title: Search
description: Client-side search that works out of the box with no API keys, plus optional hosted and semantic backends you can switch to as your docs grow.
---

Blume ships local search with no hosted infrastructure and no API keys. It runs in the browser, works in both `blume dev` and `blume build`, and indexes only your real content — navigation chrome and excluded pages are skipped. When you outgrow it, you can switch to a hosted or semantic backend without changing how search looks or behaves — only the `search.provider` you configure changes.

Blume reaches parity with Fumadocs' provider set: **Orama**, **FlexSearch**, **Algolia**, **Orama Cloud**, **Typesense**, and **Mixedbread** (plus **Pagefind**). Only the configured provider's SDK is installed into your project, so picking one backend never pulls in the others.

## Using search

Open search with <Badge variant="accent">⌘K</Badge> (or `Ctrl K`), or press `/` when you're not typing in a field. `Esc` closes it, and `⌘J` (or `Ctrl J`) toggles the result preview pane.

Queries match page **titles**, **descriptions**, and **body text**, with title matches ranked highest and descriptions above body.

## Popular pages

Before a reader types a query, the search dialog shows a **Popular** list. By default it is the first six sidebar pages — which on multi-tab sites often surfaces the wrong section. Pin the links you want instead:

```ts blume.config.ts lineNumbers
search: {
  popular: [
    { href: "/guides/getting-started", icon: "rocket", label: "Getting started" },
    { href: "/guides/install", icon: "download", label: "Install" },
    { href: "/concepts/overview", label: "Overview" },
  ],
},
```

Each entry takes an `href` (internal route or external URL) and a `label`, plus an optional `icon` — a [built-in icon](/docs/content/components#icon) name, image path/URL, or inline SVG (same _inputs_ as nav icons), defaulting to a file glyph. Omit `popular` or leave it empty to keep the sidebar fallback.

Write `href` as if the site were mounted at the root — a `basePath` is applied for you, the same as `navigation.featured`. External URLs pass through untouched.

<Callout type="warning">
  A curated list is a single set of links shared by every language. On a site
  with `i18n` configured, the sidebar fallback follows the reader's locale, but
  `popular` entries point wherever their `href` says — so pin locale-prefixed
  routes only if you want every reader sent to that one language.
</Callout>

## What's indexed

For every indexable page, Blume indexes its title, description, and body reduced to plain text — code blocks, images, and markup are stripped, so results stay relevant. The index is built from your source files, so it's identical in dev and production.

On a [versioned](/docs/content/versioning) site, results default to the version being viewed, with an "All versions" toggle in the dialog footer (remembered per reader). Cross-version hits name their version on the row. Orama, FlexSearch, Algolia, and Typesense honor the scoping — hosted records carry a `version` facet, with the current docs uploaded as `"current"` — while Pagefind stays unscoped, matching its locale behavior.

## Tags

Add `search.tags` to a page's frontmatter to group it under a filter in the search dialog — readers can narrow results to a tag with a click. Tags also become a facet on the hosted providers.

```yaml
search:
  tags: [api, reference]
```

## Providers

The client-side providers are keyless and need no extra config. The hosted ones take **public** credentials in `blume.config.ts` (safe to ship to the browser) and read their **secret** admin key from an environment variable at build time — the secret never lands in the config or the client bundle.

### Orama (default)

Blume's default engine. It builds a JSON index served at `/blume-search.json` and queries it in the browser — instant, client-side, and live in `blume dev` as you edit. No keys, no service.

```ts blume.config.ts lineNumbers
search: {
  provider: "orama", // default
}
```

#### Non-Latin scripts

Orama's standard tokenizer keeps only basic Latin letters, digits and a handful of accented vowels, so text in any other script — Japanese, Chinese, Korean and Thai, but equally Russian, Greek, Hebrew and Hindi — would otherwise produce no matches at all. Blume handles this for you: when [`i18n.defaultLocale`](/docs/content/i18n) resolves to a non-Latin script, the index switches to a word-segmenting tokenizer (built on the browser- and Node-native `Intl.Segmenter`). Declaring your site's language is all it takes:

```ts blume.config.ts lineNumbers
i18n: {
  defaultLocale: "ja",
  locales: [{ code: "ja", label: "日本語" }],
}
```

The same tokenizer serves the search dialog, the MCP server's `search_docs` tool, and Ask AI grounding. The script is what decides, not the language name — `az-Cyrl` is segmented while `sr-Latn` is not — and it is the default locale that decides for the whole index: on a mixed-language site every page shares the default locale's tokenizer. With a non-Latin default that's safe, because Latin words survive segmentation intact, so pages in English stay searchable alongside the default language. The reverse doesn't hold: non-Latin translations on a Latin-default site aren't searchable. Latin-script languages that lean heavily on diacritics (Vietnamese, or Serbian in Latin script) also fare worse on the standard tokenizer, which folds only a few accented vowels and splits words on the rest.

Japanese and Chinese go one step further. Segmenting alone indexes a compound term as its parts — 資金決済法 as 資金, 決済 and 法 — which lets a page mentioning each part somewhere outrank the page the term is actually about. Han, Hiragana and Katakana are therefore indexed as overlapping character pairs, and queries on those indexes prefer pages carrying a term's pairs together, loosening to any-pair matching when no page carries them all, so typing a whole sentence still returns its closest pages. Korean and Thai keep their segmented words.

### FlexSearch

A second keyless, client-side option. It reuses the same `/blume-search.json` index Orama ships and builds a [FlexSearch](https://github.com/nextapps-de/flexsearch) document index in the browser. Works in `blume dev` and `blume build`.

FlexSearch has no equivalent segmentation hook, so for sites in a non-Latin script prefer Orama (the default) or [Pagefind](#pagefind), whose `pagefind_extended` binary indexes a broad set of languages and segments Chinese, Japanese and Korean natively.

```ts blume.config.ts lineNumbers
search: {
  provider: "flexsearch",
}
```

### Pagefind

For very large docs, opt into [Pagefind](https://pagefind.app). It indexes your built HTML and loads the index in shards on demand, keeping the initial payload tiny no matter how big the site grows.

```ts blume.config.ts lineNumbers
search: {
  provider: "pagefind",
}
```

Pagefind only runs during `blume build`, so search isn't available in `blume dev` with this provider.

### Algolia

The browser queries [Algolia](https://www.algolia.com) directly with your search-only key. Each `blume build` replaces the index using the admin key from `ALGOLIA_ADMIN_API_KEY` (the build warns and skips the upload if it's unset). The whole index is replaced on every sync, so pages you delete or rename don't linger as stale results.

```ts blume.config.ts lineNumbers
search: {
  provider: "algolia",
  algolia: {
    appId: "YOUR_APP_ID",
    indexName: "docs",
    searchApiKey: "YOUR_SEARCH_ONLY_KEY", // public
  },
}
```

### Orama Cloud

Hosted Orama. The browser queries your index endpoint with the public API key; `blume build` pushes records to the index using `ORAMA_PRIVATE_API_KEY`. Set `indexId` to enable the sync.

```ts blume.config.ts lineNumbers
search: {
  provider: "orama-cloud",
  oramaCloud: {
    endpoint: "https://cloud.orama.run/v1/indexes/your-index",
    apiKey: "YOUR_PUBLIC_API_KEY",
    indexId: "your-index-id", // for the build-time sync
  },
}
```

### Typesense

Self-hosted or cloud [Typesense](https://typesense.org). The browser queries the collection with the search-only key; `blume build` recreates the collection and imports documents using `TYPESENSE_ADMIN_API_KEY`. The collection is dropped and rebuilt on every sync so deleted or renamed pages don't linger as stale results — if you hand-tune the collection's settings, reapply them after a build.

```ts blume.config.ts lineNumbers
search: {
  provider: "typesense",
  typesense: {
    host: "xyz.a1.typesense.net",
    collection: "docs",
    searchApiKey: "YOUR_SEARCH_ONLY_KEY", // public
    // port + protocol default to 443 / https
  },
}
```

### Mixedbread

Semantic search via [Mixedbread](https://www.mixedbread.com). Queries are proxied through a generated `/api/search` endpoint that holds your key, so this provider **requires server output** (`deployment.output: "server"`). The endpoint reads `MIXEDBREAD_API_KEY`. Sync your content to the store with the Mixedbread CLI in your build, e.g. `mxbai vs sync <STORE_ID> ./content --ci`.

```ts blume.config.ts lineNumbers
search: {
  provider: "mixedbread",
  mixedbread: {
    storeId: "YOUR_STORE_ID",
  },
}
```

### Disabling search

```ts blume.config.ts lineNumbers
search: {
  provider: "none",
}
```

## Excluding pages

Only indexable pages are searched. A page is left out of the index when it sets `search.exclude` in frontmatter:

```yaml
search:
  exclude: true
```

[Hidden pages](/docs/content/navigation#hidden-pages) are also excluded by default. To index them anyway, opt in:

```ts blume.config.ts lineNumbers
search: {
  indexing: { includeHiddenPages: true },
}
```
