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

將 Algolia Search 整合至 Fumadocs

<Callout title="注意">
若您使用 Algolia 的免費方案，則必須[在搜尋對話方塊中顯示 Algolia 的標誌](https://algolia.com/policies/free-services-terms)。
</Callout>

## 設定 [#setup]

安裝相依套件：

```package-install
algoliasearch
```

### 註冊 Algolia [#sign-up-on-algolia]

請註冊帳號並取得搜尋用的應用程式 ID 與 API 金鑰，然後將它們存放在 `.env.local` 中 (在 Vite 框架中，`NEXT_PUBLIC_` 應改為 `VITE_`；在 Astro 中則改為 `PUBLIC_`) ：

```dotenv
NEXT_PUBLIC_ALGOLIA_APP_ID="app ID"
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY="search-only API key"
ALGOLIA_WRITE_KEY="write API key"
```

### 同步搜尋索引 [#sync-search-indexes]

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

```ts tab="Next.js" title="app/static.json/route.ts"
import { source } from '@/lib/source';
import { toDocuments } from 'fumadocs-core/search/algolia';

export const revalidate = false;

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

```

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

// 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 = algoliasearch(
  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,
  process.env.ALGOLIA_WRITE_KEY!,
);

await sync(client, {
  indexName: 'document',
  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 { toDocuments } from 'fumadocs-core/search/algolia';

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

```

```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 'fumadocs-core/search/algolia';
import { algoliasearch } from 'algoliasearch';

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

const client = algoliasearch(
  process.env.VITE_ALGOLIA_APP_ID!,
  process.env.ALGOLIA_WRITE_KEY!,
);

await sync(client, {
  indexName: 'document',
  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 { toDocuments } from 'fumadocs-core/search/algolia';

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

```

```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 'fumadocs-core/search/algolia';
import { algoliasearch } from 'algoliasearch';

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

const client = algoliasearch(
  process.env.VITE_ALGOLIA_APP_ID!,
  process.env.ALGOLIA_WRITE_KEY!,
);

await sync(client, {
  indexName: 'document',
  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 { toDocuments } from 'fumadocs-core/search/algolia';

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

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 'fumadocs-core/search/algolia';
import { algoliasearch } from 'algoliasearch';

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

const client = algoliasearch(
  process.env.VITE_ALGOLIA_APP_ID!,
  process.env.ALGOLIA_WRITE_KEY!,
);

await sync(client, {
  indexName: 'document',
  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 { toDocuments } from 'fumadocs-core/search/algolia';

export const prerender = true;

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

```

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

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

const client = algoliasearch(
  process.env.PUBLIC_ALGOLIA_APP_ID!,
  process.env.ALGOLIA_WRITE_KEY!,
);

await sync(client, {
  indexName: 'document',
  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"
  }
}
```

`sync()` 會更新索引設定並取代所有紀錄。`build` 指令碼會在每次建置時執行同步，因此請在您的部署平台上設定相同的變數。

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

您可以考慮以下幾種實作 UI 的方式：

* 使用 [Fumadocs UI 搜尋對話方塊](/docs/next/search/algolia)。

* 透過內建的搜尋用戶端 hook 自行建構：

  ```ts twoslash
  import { liteClient } from 'algoliasearch/lite';
  import { useAlgoliaSearch } from 'fumadocs-core/search/client';

  const algolia = liteClient('id', 'key');

  const { search, onSearchChange, isLoading, data } = useAlgoliaSearch({
    indexName: 'document',
    client: algolia,
  });
  ```

* 直接使用 Algolia 的官方用戶端。

## 進階 [#advanced]

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

若要設定標籤篩選，請在 `static.json` 路由中將 `tag` 傳入 `toDocuments()`：

```ts
toDocuments(source, { tag: (page) => page.slugs[0] });
```

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

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

  ```ts
  import { useAlgoliaSearch } from 'fumadocs-core/search/client';

  const { search, onSearchChange, isLoading, data } = useAlgoliaSearch({
    tag: '<your tag value>',
    // ...
  });
  ```

`tag` 欄位是用於分面搜尋 (faceting) 的屬性。您也可以在 Algolia 搜尋用戶端中使用 `tag:value` 篩選條件。

### 底層運作原理 [#under-the-hood]

Algolia 整合會自動設定 Algolia Search，用於文件搜尋。

它會為文件中的**每個段落**各建立一筆紀錄，這也是 Algolia 建議的做法。

每筆紀錄都包含以下可搜尋的屬性：

| 屬性        | 說明               |
| --------- | ---------------- |
| `title`   | 頁面標題             |
| `section` | 標題 ID (可為 null)  |
| `content` | 段落內容             |

只有位於標題下方的段落才會有 `section` 欄位。標題與
段落會分別索引為獨立的紀錄，並依頁面 ID 分組。

請注意，頁面的 `url` 屬性必須是唯一的，請勿讓兩個頁面使用相同的
URL。