# Fumadocs Core (the core library of Fumadocs): Typesense 搜尋
URL: /zh-TW/docs/headless/search/typesense
Source: https://raw.githubusercontent.com/fuma-nama/fumadocs/refs/heads/main/apps/docs/content/docs/headless/search/typesense.zh-TW.mdx

將 Typesense 搜尋整合至 Fumadocs

> 此整合由社群維護。

## 設定 [#setup]

### 安裝相依套件 [#install-dependencies]

```package-install
typesense typesense-fumadocs-adapter
```

### 啟動 Typesense 伺服器 [#start-typesense-server]

您可以自行架設 Typesense 伺服器，或使用其雲端服務。請依照官方的[入門指南](https://typesense.org/docs/guide/install-typesense.html)設定伺服器，並將認證資訊儲存在 `.env.local` 中 (在 Vite 框架中請將 `NEXT_PUBLIC_` 改為 `VITE_`，在 Astro 中則改為 `PUBLIC_`) ：

```dotenv
NEXT_PUBLIC_TYPESENSE_URL="server URL"
NEXT_PUBLIC_TYPESENSE_SEARCH_KEY="search-only API key"
TYPESENSE_API_KEY="API key with write access"
```

### 同步資料集 [#sync-dataset]

透過預先算繪的 `static.json` 路由匯出搜尋索引，並在每次建置後以指令碼將其同步至 Typesense：

```ts tab="Next.js" title="app/static.json/route.ts"
import { source } from '@/lib/source';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

async function getDocuments() {
  const documents: DocumentRecord[] = [];
  for (const page of source.getPages()) {
    documents.push({
      _id: page.url,
      structured: page.data.structuredData,
      url: page.url,
      title: page.data.title,
      description: page.data.description,
      locale: page.locale,
    });
  }
  return documents;
}

export const revalidate = false;

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

```

```ts tab="Next.js" title="scripts/sync-content.ts"
import * as fs from 'node:fs/promises';
import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';
import { Client } from 'typesense';

// Next.js writes it under `route-cache` when built with an adapter, like on Vercel
const [file] = await Array.fromAsync(fs.glob('.next/server/{app,route-cache/*/*/$}/static.json.body'));
const records = JSON.parse(await fs.readFile(file, 'utf-8')) as DocumentRecord[];

const client = new Client({
  nodes: [{ url: process.env.NEXT_PUBLIC_TYPESENSE_URL! }],
  apiKey: process.env.TYPESENSE_API_KEY!,
  connectionTimeoutSeconds: 60 * 15,
});

await sync(client, {
  typesenseCollectionName: 'docs',
  documents: records,
});

console.log(`search updated: ${records.length} records`);

```

```json tab="Next.js" title="package.json"
{
  "scripts": {
    "build": "next build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="React Router" title="app/routes/static.json.ts"
import { source } from '@/lib/source';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

async function getDocuments() {
  const documents: DocumentRecord[] = [];
  for (const page of source.getPages()) {
    documents.push({
      _id: page.url,
      structured: await page.data.structuredData(),
      url: page.url,
      title: page.data.title,
      description: page.data.description,
      locale: page.locale,
    });
  }
  return documents;
}

export async function loader() {
  return Response.json(await getDocuments());
}

```

```ts tab="React Router" title="app/routes.ts"
import { index, route, type RouteConfig } from '@react-router/dev/routes';

export default [
  index('routes/home.tsx'),
  route('static.json', 'routes/static.json.ts'), // [!code ++:1]
] satisfies RouteConfig;

```

```ts tab="React Router" title="scripts/sync-content.ts"
import * as fs from 'node:fs/promises';
import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';
import { Client } from 'typesense';

const records = JSON.parse(await fs.readFile('build/client/static.json', 'utf-8')) as DocumentRecord[];

const client = new Client({
  nodes: [{ url: process.env.VITE_TYPESENSE_URL! }],
  apiKey: process.env.TYPESENSE_API_KEY!,
  connectionTimeoutSeconds: 60 * 15,
});

await sync(client, {
  typesenseCollectionName: 'docs',
  documents: records,
});

console.log(`search updated: ${records.length} records`);

```

```json tab="React Router" title="package.json"
{
  "scripts": {
    "build": "react-router build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="TanStack Start" title="src/routes/static[.]json.ts"
import { createFileRoute } from '@tanstack/react-router';
import { source } from '@/lib/source';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

async function getDocuments() {
  const documents: DocumentRecord[] = [];
  for (const page of source.getPages()) {
    documents.push({
      _id: page.url,
      structured: await page.data.structuredData(),
      url: page.url,
      title: page.data.title,
      description: page.data.description,
      locale: page.locale,
    });
  }
  return documents;
}

export const Route = createFileRoute('/static.json')({
  server: {
    handlers: {
      GET: async () => Response.json(await getDocuments()),
    },
  },
});

```

```ts tab="TanStack Start" title="vite.config.ts"
import { tanstackStart } from '@tanstack/react-start/plugin/vite';
import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    tanstackStart({
      prerender: {
        enabled: true,
      },
      pages: [ // [!code ++:3]
        { path: '/static.json' }
      ],
    }),
    nitro({
      preset: 'vercel',
    }),
  ],
});

```

```ts tab="TanStack Start" title="scripts/sync-content.ts"
import * as fs from 'node:fs/promises';
import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';
import { Client } from 'typesense';

const records = JSON.parse(await fs.readFile('.vercel/output/static/static.json', 'utf-8')) as DocumentRecord[];

const client = new Client({
  nodes: [{ url: process.env.VITE_TYPESENSE_URL! }],
  apiKey: process.env.TYPESENSE_API_KEY!,
  connectionTimeoutSeconds: 60 * 15,
});

await sync(client, {
  typesenseCollectionName: 'docs',
  documents: records,
});

console.log(`search updated: ${records.length} records`);

```

```json tab="TanStack Start" title="package.json"
{
  "scripts": {
    "build": "vite build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="Waku" title="src/pages/_api/static.json.ts"
import { source } from '@/lib/source';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

async function getDocuments() {
  const documents: DocumentRecord[] = [];
  for (const page of source.getPages()) {
    documents.push({
      _id: page.url,
      structured: page.data.structuredData,
      url: page.url,
      title: page.data.title,
      description: page.data.description,
      locale: page.locale,
    });
  }
  return documents;
}

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

export async function getConfig() {
  return {
    render: 'static' as const,
  } as const;
}

```

```ts tab="Waku" title="scripts/sync-content.ts"
import * as fs from 'node:fs/promises';
import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';
import { Client } from 'typesense';

const records = JSON.parse(await fs.readFile('dist/public/static.json', 'utf-8')) as DocumentRecord[];

const client = new Client({
  nodes: [{ url: process.env.VITE_TYPESENSE_URL! }],
  apiKey: process.env.TYPESENSE_API_KEY!,
  connectionTimeoutSeconds: 60 * 15,
});

await sync(client, {
  typesenseCollectionName: 'docs',
  documents: records,
});

console.log(`search updated: ${records.length} records`);

```

```json tab="Waku" title="package.json"
{
  "scripts": {
    "build": "waku build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

```ts tab="Astro" title="src/pages/static.json.ts"
import type { APIRoute } from 'astro';
import { source } from '@/lib/source';
import type { DocumentRecord } from 'typesense-fumadocs-adapter';

async function getDocuments() {
  const documents: DocumentRecord[] = [];
  for (const page of source.getPages()) {
    documents.push({
      _id: page.url,
      structured: await page.data.structuredData(),
      url: page.url,
      title: page.data.title,
      description: page.data.description,
      locale: page.locale,
    });
  }
  return documents;
}

export const prerender = true;

export const GET: APIRoute = async () => Response.json(await getDocuments());

```

```ts tab="Astro" title="scripts/sync-content.ts"
import * as fs from 'node:fs/promises';
import { type DocumentRecord, sync } from 'typesense-fumadocs-adapter';
import { Client } from 'typesense';

const records = JSON.parse(await fs.readFile('dist/static.json', 'utf-8')) as DocumentRecord[];

const client = new Client({
  nodes: [{ url: process.env.PUBLIC_TYPESENSE_URL! }],
  apiKey: process.env.TYPESENSE_API_KEY!,
  connectionTimeoutSeconds: 60 * 15,
});

await sync(client, {
  typesenseCollectionName: 'docs',
  documents: records,
});

console.log(`search updated: ${records.length} records`);

```

```json tab="Astro" title="package.json"
{
  "scripts": {
    "build": "astro build && node --env-file-if-exists=.env.local scripts/sync-content.ts"
  }
}
```

`build` 指令碼會在每次建置時執行同步，因此請在您的部署平台上設定相同的變數。

### 搜尋 UI [#search-ui]

若要實作搜尋 UI，您可以：

* 使用 [Fumadocs UI 搜尋對話方塊](/docs/next/search/typesense)。
* 使用 Typesense 搜尋用戶端 hook，自行打造搜尋 UI。

  ```ts
  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: 'docs',
    client,
  });
  ```

## 選項 [#options]

### 標籤篩選 [#tag-filter]

若要設定標籤篩選，請在 `static.json` 路由中為各份文件加上 `tag` 值：

```ts
documents.push({
  // 其他欄位……
  // [!code ++]
  tag: '<your value>',
});
```

接著更新您的搜尋用戶端：

* **Fumadocs UI**：在搜尋 UI 中啟用[標籤篩選](/docs/next/search/typesense#tag-filter)。
* **搜尋用戶端**：您可以依下列方式加入標籤篩選：

  ```ts
  import { useTypesenseSearch } from 'typesense-fumadocs-adapter/client';
  const { search, setSearch, query } = useTypesenseSearch({
    tag: '<your tag value>',
    // ...
  });
  ```

### 國際化 (i18n)  [#internationalization-i18n]

文件會包含所屬頁面的 `locale`，請將 `locale` 傳入搜尋用戶端：

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

const { locale } = useI18n();
const { search, setSearch, query } = useTypesenseSearch({
  // [!code ++]
  locale,
  // 其他欄位...
});
```

在底層，Typesense 會為每個語系分別建立集合，集合名稱會加上 `_{locale}` 後綴 (例如 `docs_en`、`docs_fr`) 。