Core: Deprecate other parameters for useDocsSearch()

This commit is contained in:
Fuma Nama
2025-06-04 21:48:28 +08:00
parent db7cbf7833
commit a66886b077
17 changed files with 229 additions and 132 deletions
+19
View File
@@ -0,0 +1,19 @@
---
'fumadocs-core': patch
---
**Deprecate other parameters for `useDocsSearch()`**
The new usage passes options to a single object, improving the readability:
```ts
import { useDocsSearch } from 'fumadocs-core/search/client';
const { search, setSearch, query } = useDocsSearch({
type: 'fetch',
locale: 'optional',
tag: 'optional',
delayMs: 100,
allowEmpty: false
});
```
+4 -3
View File
@@ -26,12 +26,13 @@ const client = new OramaClient({
});
export default function CustomSearchDialog(props: SharedProps) {
const mode = useMode();
const [tag, setTag] = useState<string | undefined>(mode);
const { search, setSearch, query } = useDocsSearch({
type: 'orama-cloud',
client,
tag,
});
const mode = useMode();
const [tag, setTag] = useState<string | undefined>(mode);
useOnChange(mode, () => {
if (mode) setTag(mode);
@@ -55,7 +56,7 @@ export default function CustomSearchDialog(props: SharedProps) {
<SearchDialogList items={query.data} />
)}
<SearchDialogFooter className="flex flex-row">
<TagsList tag={tag} onTagChange={setTag}>
<TagsList tag={tag} onTagChange={setTag} allowClear>
<TagsListItem value="ui">Framework</TagsListItem>
<TagsListItem value="headless">Core</TagsListItem>
<TagsListItem value="mdx">MDX</TagsListItem>
@@ -146,11 +146,10 @@ And update your search client:
```ts
import { useDocsSearch } from 'fumadocs-core/search/client';
const { search, setSearch, query } = useDocsSearch(
{ ... },
undefined,
'<your tag value>', // [!code ++]
);
const { search, setSearch, query } = useDocsSearch({
tag: '<your tag value>',
// ...
});
```
The `tag` field is an attribute for faceting. You can also use the filter `tag:value` on Algolia search clients.
@@ -125,13 +125,10 @@ and update your search client:
import { useDocsSearch } from 'fumadocs-core/search/client';
// Pass `tag` in your custom search dialog
const client = useDocsSearch(
{
type: 'fetch',
},
undefined, // locale code, can be `undefined`
'tag',
);
const client = useDocsSearch({
type: 'fetch',
tag: '<value>',
});
```
### Internationalization
@@ -156,14 +153,14 @@ and update your search clients:
- **Search Client**:
Add `locale` to the search client, this will only allow pages with specified locale to be searchable by the user.
```ts
const { search, setSearch, query } = useDocsSearch(
{
type: 'fetch',
},
locale,
);
```
```ts
import { useDocsSearch } from 'fumadocs-core/search/client';
const { search, setSearch, query } = useDocsSearch({
type: 'fetch',
locale: 'cn',
});
```
### Static Export
@@ -11,10 +11,14 @@ import {
TagsListItem,
} from 'fumadocs-ui/components/dialog/search';
import { useState } from 'react';
import { useDocsSearch } from 'fumadocs-core/search/client';
export default function CustomSearchDialog(props: SharedProps) {
// [!code ++]
const [tag, setTag] = useState<string | undefined>('default-value');
const [tag, setTag] = useState<string | undefined>();
const { search, setSearch, query } = useDocsSearch({
tag, // [!code ++]
});
return (
<SearchDialog>
@@ -13,16 +13,19 @@ import {
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
const appId = 'replace me';
const apiKey = 'replace me';
const client = liteClient(appId, apiKey);
export default function CustomSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { search, setSearch, query } = useDocsSearch({
type: 'algolia',
client,
indexName: 'document',
locale,
});
return (
@@ -11,10 +11,13 @@ import {
SearchDialogOverlay,
type SharedProps,
} from 'fumadocs-ui/components/dialog/search';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
export default function DefaultSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { search, setSearch, query } = useDocsSearch({
type: 'fetch',
locale,
});
return (
@@ -14,6 +14,7 @@ import {
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { OramaClient } from '@oramacloud/client';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
const client = new OramaClient({
endpoint: 'Endpoint URL',
@@ -21,9 +22,11 @@ const client = new OramaClient({
});
export default function CustomSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { search, setSearch, query } = useDocsSearch({
type: 'orama-cloud',
client,
locale,
});
return (
@@ -12,6 +12,7 @@ import {
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { create } from '@orama/orama';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
function initOrama() {
return create({
@@ -20,9 +21,11 @@ function initOrama() {
}
export default function DefaultSearchDialog(props: SharedProps) {
const { locale } = useI18n(); // (optional) for i18n
const { search, setSearch, query } = useDocsSearch({
type: 'static',
initOrama,
locale,
});
return (
+53 -24
View File
@@ -3,7 +3,7 @@ import { useDebounce } from '@/utils/use-debounce';
import type { SortedResult } from '@/server/types';
import { type FetchOptions } from '@/search/client/fetch';
import { useOnChange } from '@/utils/use-on-change';
import { type StaticClient, type StaticOptions } from '@/search/client/static';
import { type StaticOptions } from '@/search/client/static';
import { type AlgoliaOptions } from '@/search/client/algolia';
import { type OramaCloudOptions } from '@/search/client/orama-cloud';
@@ -31,8 +31,6 @@ export type Client =
type: 'orama-cloud';
} & OramaCloudOptions);
let staticClient: StaticClient | undefined;
function isDifferentDeep(a: unknown, b: unknown): boolean {
if (Array.isArray(a) && Array.isArray(b)) {
return b.length !== a.length || a.some((v, i) => isDifferentDeep(v, b[i]));
@@ -57,22 +55,53 @@ function isDifferentDeep(a: unknown, b: unknown): boolean {
* Provide a hook to query different official search clients.
*
* Note: it will re-query when its parameters changed, make sure to use `useCallback()` on functions passed to this hook.
*
* @param client - search client
* @param locale - Filter with locale
* @param tag - Filter with specific tag
* @param delayMs - The debounced delay for performing a search.
* @param allowEmpty - still perform search even if query is empty
* @param _key - cache key (deprecated)
*/
export function useDocsSearch(
client: Client,
locale?: string,
tag?: string,
delayMs = 100,
allowEmpty = false,
clientOptions: Client & {
/**
* The debounced delay for performing a search (in ms).
* .
* @defaultValue 100
*/
delayMs?: number;
/**
* still perform search even if query is empty.
*
* @defaultValue false
*/
allowEmpty?: boolean;
},
/**
* @deprecated pass to `client` object instead
*/
_locale?: string,
/**
* @deprecated pass to `client` object instead
*/
_tag?: string,
/**
* @deprecated pass to `client` object instead
*/
_delayMs = 100,
/**
* @deprecated pass to `client` object instead
*/
_allowEmpty = false,
/**
* @deprecated No longer used
*/
_key?: string,
): UseDocsSearch {
// handle deprecated params
const {
delayMs = _delayMs ?? 100,
allowEmpty = _allowEmpty ?? false,
...client
} = clientOptions;
client.tag ??= _tag;
client.locale ??= _locale;
const [search, setSearch] = useState('');
const [results, setResults] = useState<SortedResult[] | 'empty'>('empty');
const [error, setError] = useState<Error>();
@@ -81,7 +110,7 @@ export function useDocsSearch(
const onStart = useRef<() => void>(undefined);
useOnChange(
[client, debouncedValue, locale, tag],
[client, debouncedValue],
() => {
if (onStart.current) {
onStart.current();
@@ -99,25 +128,25 @@ export function useDocsSearch(
if (client.type === 'fetch') {
const { fetchDocs } = await import('./client/fetch');
return fetchDocs(debouncedValue, locale, tag, client);
return fetchDocs(debouncedValue, client);
}
if (client.type === 'algolia') {
const { searchDocs } = await import('./client/algolia');
return searchDocs(debouncedValue, tag, locale, client);
return searchDocs(debouncedValue, client);
}
if (client.type === 'orama-cloud') {
const { searchDocs } = await import('./client/orama-cloud');
return searchDocs(debouncedValue, tag, client);
return searchDocs(debouncedValue, client);
}
const { createStaticClient } = await import('./client/static');
if (!staticClient) staticClient = createStaticClient(client);
if (client.type === 'static') {
const { search } = await import('./client/static');
return search(debouncedValue, client);
}
return staticClient.search(debouncedValue, locale, tag);
throw new Error('unknown search client');
}
void run()
+8 -3
View File
@@ -6,6 +6,13 @@ export interface AlgoliaOptions {
indexName: string;
client: LiteClient;
/**
* Filter results with specific tag.
*/
tag?: string;
locale?: string;
onSearch?: (
query: string,
tag?: string,
@@ -44,9 +51,7 @@ export function groupResults(hits: Hit<BaseIndex>[]): SortedResult[] {
export async function searchDocs(
query: string,
tag: string | undefined,
locale: string | undefined,
{ indexName, onSearch, client }: AlgoliaOptions,
{ indexName, onSearch, client, locale, tag }: AlgoliaOptions,
): Promise<SortedResult[]> {
if (query.length > 0) {
const result = onSearch
+14 -4
View File
@@ -3,24 +3,34 @@ import type { SortedResult } from '@/server';
export interface FetchOptions {
/**
* API route for search endpoint
*
* @defaultValue '/api/search'
*/
api?: string;
/**
* Filter results with specific tag.
*/
tag?: string;
/**
* Filter by locale
*/
locale?: string;
}
const cache = new Map<string, SortedResult[]>();
export async function fetchDocs(
query: string,
locale: string | undefined,
tag: string | undefined,
options: FetchOptions,
{ api = '/api/search', locale, tag }: FetchOptions,
): Promise<SortedResult[]> {
const params = new URLSearchParams();
params.set('query', query);
if (locale) params.set('locale', locale);
if (tag) params.set('tag', tag);
const key = `${options.api ?? '/api/search'}?${params}`;
const key = `${api}?${params}`;
const cached = cache.get(key);
if (cached) return cached;
+12 -3
View File
@@ -1,5 +1,5 @@
import type { SortedResult } from '@/server';
import type { OramaClient, ClientSearchParams } from '@oramacloud/client';
import type { ClientSearchParams, OramaClient } from '@oramacloud/client';
import { removeUndefined } from '@/utils/remove-undefined';
import type { OramaIndex } from '@/search/orama-cloud';
@@ -20,15 +20,24 @@ export interface OramaCloudOptions {
*/
index?: 'default' | 'crawler';
params?: ClientSearchParams;
/**
* Filter results with specific tag.
*/
tag?: string;
/**
* Filter by locale (unsupported at the moment)
*/
locale?: string;
}
export async function searchDocs(
query: string,
tag: string | undefined,
options: OramaCloudOptions,
): Promise<SortedResult[]> {
const list: SortedResult[] = [];
const { index = 'default', client, params: extraParams = {} } = options;
const { index = 'default', client, params: extraParams = {}, tag } = options;
if (index === 'crawler') {
const result = await client.search({
+61 -52
View File
@@ -1,5 +1,4 @@
import { type AnyOrama, create, load, type Orama } from '@orama/orama';
import { type SortedResult } from '@/server';
import { searchSimple } from '@/search/orama/search/simple';
import { searchAdvanced } from '@/search/orama/search/advanced';
import {
@@ -11,34 +10,44 @@ import type { ExportedData } from '@/search/server';
export interface StaticOptions {
/**
* Where to download exported search indexes (URL)
*
* @defaultValue '/api/search'
*/
from?: string;
initOrama?: (locale?: string) => AnyOrama | Promise<AnyOrama>;
/**
* Filter results with specific tag.
*/
tag?: string;
/**
* Filter by locale (unsupported at the moment)
*/
locale?: string;
}
export interface StaticClient {
search: (
query: string,
locale: string | undefined,
tag: string | undefined,
) => Promise<SortedResult[]>;
}
const cache = new Map<string, Promise<Database>>();
export function createStaticClient({
// locale -> db
type Database = Map<
string,
{
type: 'simple' | 'advanced';
db: AnyOrama;
}
>;
async function loadDB({
from = '/api/search',
initOrama = (locale) => create({ schema: { _: 'string' }, language: locale }),
}: StaticOptions): StaticClient {
// locale -> db
const dbs = new Map<
string,
{
type: 'simple' | 'advanced';
db: AnyOrama;
}
>();
}: StaticOptions): Promise<Database> {
const cacheKey = from;
const cached = cache.get(cacheKey);
if (cached) return cached;
async function init(): Promise<void> {
async function init() {
const res = await fetch(from);
if (!res.ok)
@@ -47,46 +56,46 @@ export function createStaticClient({
);
const data = (await res.json()) as ExportedData;
const dbs: Database = new Map();
if (data.type === 'i18n') {
for (const [k, v] of Object.entries(data.data)) {
const db = await initOrama(k);
await Promise.all(
Object.entries(data.data).map(async ([k, v]) => {
const db = await initOrama(k);
load(db, v);
dbs.set(k, {
type: v.type,
db,
});
}
} else {
const db = await initOrama();
load(db, v);
dbs.set(k, {
type: v.type,
db,
});
}),
);
load(db, data);
dbs.set('', {
type: data.type,
db,
});
return dbs;
}
const db = await initOrama();
load(db, data);
dbs.set('', {
type: data.type,
db,
});
return dbs;
}
const get = init();
return {
async search(query, locale, tag) {
await get;
const cached = dbs.get(locale ?? '');
const result = init();
cache.set(cacheKey, result);
return result;
}
if (!cached) return [];
if (cached.type === 'simple')
return searchSimple(
cached as unknown as Orama<typeof simpleSchema>,
query,
);
export async function search(query: string, options: StaticOptions) {
const { tag, locale } = options;
return searchAdvanced(
cached.db as Orama<typeof advancedSchema>,
query,
tag,
);
},
};
const db = (await loadDB(options)).get(locale ?? '');
if (!db) return [];
if (db.type === 'simple')
return searchSimple(db as unknown as Orama<typeof simpleSchema>, query);
return searchAdvanced(db.db as Orama<typeof advancedSchema>, query, tag);
}
@@ -21,6 +21,7 @@ import {
} from './search';
import type { SortedResult } from 'fumadocs-core/server';
import type { SearchLink, SharedProps, TagItem } from '@/contexts/search';
import { useI18n } from '@/contexts/i18n';
export interface AlgoliaSearchDialogProps extends SharedProps {
searchOptions: AlgoliaOptions;
@@ -55,16 +56,15 @@ export default function AlgoliaSearchDialog({
links = [],
footer,
...props
}: AlgoliaSearchDialogProps): ReactNode {
}: AlgoliaSearchDialogProps) {
const [tag, setTag] = useState(defaultTag);
const { search, setSearch, query } = useDocsSearch(
{
type: 'algolia',
...searchOptions,
},
undefined,
const { locale } = useI18n();
const { search, setSearch, query } = useDocsSearch({
type: 'algolia',
tag,
);
locale,
...searchOptions,
});
const defaultItems = useMemo<SortedResult[]>(
() =>
links.map(([name, link]) => ({
@@ -61,7 +61,7 @@ export default function DefaultSearchDialog({
links = [],
footer,
...props
}: DefaultSearchDialogProps): ReactNode {
}: DefaultSearchDialogProps) {
const { locale } = useI18n();
const [tag, setTag] = useState(defaultTag);
const { search, setSearch, query } = useDocsSearch(
@@ -69,14 +69,17 @@ export default function DefaultSearchDialog({
? {
type: 'fetch',
api,
locale,
tag,
delayMs,
}
: {
type: 'static',
from: api,
locale,
tag,
delayMs,
},
locale,
tag,
delayMs,
);
const defaultItems = useMemo<SortedResult[]>(
() =>
@@ -21,6 +21,7 @@ import {
} from './search';
import type { SortedResult } from 'fumadocs-core/server';
import type { SearchLink, SharedProps, TagItem } from '@/contexts/search';
import { useI18n } from '@/contexts/i18n';
export interface OramaSearchDialogProps extends SharedProps {
links?: SearchLink[];
@@ -62,18 +63,17 @@ export default function OramaSearchDialog({
footer,
links = [],
...props
}: OramaSearchDialogProps): ReactNode {
}: OramaSearchDialogProps) {
const { locale } = useI18n();
const [tag, setTag] = useState(defaultTag);
const { search, setSearch, query } = useDocsSearch(
{
type: 'orama-cloud',
client,
index,
params: searchOptions,
},
undefined,
const { search, setSearch, query } = useDocsSearch({
type: 'orama-cloud',
client,
index,
params: searchOptions,
locale,
tag,
);
});
const defaultItems = useMemo<SortedResult[]>(
() =>