Meilisearch
Integrate Meilisearch with Fumadocs
Meilisearch is an open-source search engine with typo tolerance and filtering, you can self-host it or use Meilisearch Cloud.
Setup
Install dependencies:
npm install meilisearchAPI Keys
Start a Meilisearch instance by self-hosting or on Meilisearch Cloud, and store its credentials in .env.local (VITE_ instead of NEXT_PUBLIC_ outside Next.js):
NEXT_PUBLIC_MEILISEARCH_HOST="URL of your instance"
NEXT_PUBLIC_MEILISEARCH_SEARCH_KEY="search API key"
MEILISEARCH_ADMIN_KEY="Default Admin API Key"The search key is exposed to the browser, create one that can only search the docs index. Keep the admin key private.
Sync Documents
Pre-render a static.json route to export the search documents on build, and sync them to Meilisearch with a script after each build:
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/meilisearch';
export const revalidate = false;
export async function GET() {
return Response.json(await toDocuments(source));
}
import * as fs from 'node:fs/promises';
import { type DocumentRecord, sync } from 'fumadocs-core/search/meilisearch';
import { Meilisearch } from 'meilisearch';
const records = JSON.parse(await fs.readFile('.next/server/app/static.json.body', 'utf-8')) as DocumentRecord[];
const client = new Meilisearch({
host: process.env.NEXT_PUBLIC_MEILISEARCH_HOST!,
apiKey: process.env.MEILISEARCH_ADMIN_KEY!,
});
await sync(client, {
indexName: 'docs',
documents: records,
});
console.log(`search updated: ${records.length} records`);
{
"scripts": {
"build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
}
}sync() configures the index and replaces its documents, the old documents stay searchable until the new ones are indexed. The build script syncs on every build, set the same variables on your deployment platform.
Search UI
You can consider different options for implementing the UI:
-
Using Fumadocs UI search dialog.
-
Build your own using the built-in search client hook:
import { } from 'meilisearch'; import { } from 'fumadocs-core/search/client'; import { } from 'fumadocs-core/search/client/meilisearch'; const = new ({ : 'host url', : 'search api key', }); const { , , } = ({ : ({ , : 'docs', }), });Prop
Type
-
Use their official clients directly.
Options
Tag Filter
To configure tag filtering, pass a tag to toDocuments() in the static.json route:
toDocuments(source, { tag: (page) => page.slugs[0] });And update your search client:
-
Fumadocs UI: Enable Tag Filter on Search UI.
-
Search Client: pass a tag to
meilisearchClient().import { useDocsSearch } from 'fumadocs-core/search/client'; import { meilisearchClient } from 'fumadocs-core/search/client/meilisearch'; const { search, setSearch, query } = useDocsSearch({ client: meilisearchClient({ tag: '<your tag value>', // ... }), });
Internationalization
Documents include the locale of their pages, pass locale to search in a language:
import { useI18n } from 'fumadocs-ui/contexts/i18n';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { meilisearchClient } from 'fumadocs-core/search/client/meilisearch';
const { locale } = useI18n();
const { search, setSearch, query } = useDocsSearch({
client: meilisearchClient({
locale,
// ...
}),
});Index Settings
sync() only sets searchableAttributes and filterableAttributes, other settings like synonyms and typo tolerance are kept. You can tune them from the Meilisearch dashboard or the meilisearch client.
Under the Hood
Each heading and paragraph is a document, with the page title, URL and breadcrumbs. Only content is searchable, the search client groups results by page.
| Attribute | Description |
|---|---|
type | page, heading or text |
content | Page title, heading or paragraph |
heading | ID of the heading it belongs to (optional) |
tag | Tag(s) to filter results (optional) |
locale | Locale of page (optional) |
How is this guide?
Last updated on
