Fumadocs

Typesense Search

Integrate Typesense Search with Fumadocs

This is a community-maintained integration.

Setup

Install Dependencies

npm install typesense typesense-fumadocs-adapter

Start Typesense Server

You can either self-host the Typesense server or use their cloud service. Follow their getting started guide to set up your server and obtain the API key and server URL.

Sync Dataset

Export the search indexes by pre-rendering a static route.

lib/export-search-indexes.ts
import { source } from '@/lib/source';
import { findPath } from 'fumadocs-core/page-tree';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

export async function exportSearchIndexes() {
  const results: DocumentRecord[] = [];

  function isBreadcrumbItem(item: unknown): item is string {
    return typeof item === 'string' && item.length > 0;
  }

  for (const page of source.getPages()) {
    let breadcrumbs: string[] | undefined;
    const pageTree = source.getPageTree(page.locale);
    const path = findPath(
      pageTree.children,
      (node) => node.type === 'page' && node.url === page.url,
    );

    if (path) {
      breadcrumbs = [];
      path.pop();
      if (isBreadcrumbItem(pageTree.name)) {
        breadcrumbs.push(pageTree.name);
      }
      for (const segment of path) {
        if (!isBreadcrumbItem(segment.name)) continue;
        breadcrumbs.push(segment.name);
      }
    }

    results.push({
      _id: page.url,
      structured: page.data.structuredData,
      url: page.url,
      title: page.data.title,
      description: page.data.description,
      breadcrumbs,
      locale: page.locale,
    });
  }

  return results;
}
app/static.json/route.ts
import { exportSearchIndexes } from '@/lib/export-search-indexes';

export const revalidate = false;

export async function GET() {
  return Response.json(await exportSearchIndexes());
}

Create a script to sync search indexes:

scripts/sync-content.ts
import * as fs from 'node:fs';
import { sync, DocumentRecord } from 'typesense-fumadocs-adapter';
import { Client } from 'typesense';

const filePath = '<see below>';
const content = fs.readFileSync(filePath);

const records = JSON.parse(content.toString()) as DocumentRecord[];

const client = new Client({
  nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }],
  apiKey: 'YOUR_TYPESENSE_API_KEY_WITH_WRITE_ACCESS',
  connectionTimeoutSeconds: 60 * 15,
});

// update the collection settings and sync search indexes
void sync(client, {
  typesenseCollectionName: 'YOUR_COLLECTION_NAME',
  documents: records,
});

filePath refers to the path of pre-rendered static.json, choose one according to your setup:

const filePath = '.next/server/app/static.json.body';

Make sure to run the script after build:

package.json
{
  "scripts": {
    "build": "... && bun scripts/sync-content.ts"
  }
}

You can also integrate it with your CI/CD pipeline.

Search UI

To implement the search UI, you can either:

  • Build your own search UI with the Typesense search client hook.
    import { Client } from 'typesense';
    import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client';
    
    const client = new Client({
      nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }],
      apiKey: 'YOUR_TYPESENSE_SEARCH_ONLY_API_KEY',
    });
    
    const { search, setSearch, query } = useTypesenseSearch({
      typesenseCollectionName: 'YOUR_COLLECTION_NAME',
      client,
    });

Options

Tag Filter

To configure tag filtering, add a tag value to indexes.

lib/export-search-indexes.ts
import { source } from '@/lib/source';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

export async function exportSearchIndexes() {
  const results: DocumentRecord[] = [];

  for (const page of source.getPages()) {
    results.push({
      // other fields...
      tag: '<your value>',
    });
  }

  return results;
}

And update your search client:

  • Fumadocs UI: Enable Tag Filter on Search UI.
  • Search Client: You can add the tag filter like:
    import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client';
    const { search, setSearch, query } = useTypesenseSearch({
      tag: '<your tag value>',
      // ...
    });

Internationalization (i18n)

To support internationalization, make sure to add a locale value to each document.

lib/export-search-indexes.ts
import { source } from '@/lib/source';
import { findPath } from 'fumadocs-core/page-tree';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

export async function exportSearchIndexes() {
  const results: DocumentRecord[] = [];

  for (const page of source.getPages()) {
    results.push({
      // other fields...
      locale: page.locale,
    });
  }

  return results;
}

And update your search client:

import { useI18n } from 'fumadocs-ui/contexts/i18n';
import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client';

const { locale } = useI18n();
const { search, setSearch, query } = useTypesenseSearch({
  locale,
  // other fields...
});

Under the hood, Typesense will create separate collections for each locale. The collection name is appended with the _{locale} suffix (e.g., YOUR_COLLECTION_NAME_en, YOUR_COLLECTION_NAME_fr).

How is this guide?

Last updated on

On this page