Skip to content

Commit 3793d16

Browse files
authored
feat(plugin-algolia): add plugin-algolia and optimize some DX on customizing Search (#1909)
1 parent a2fec82 commit 3793d16

File tree

28 files changed

+1429
-58
lines changed

28 files changed

+1429
-58
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Search algolia
+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "@rspress-fixture/rspress-algolia",
3+
"version": "1.0.0",
4+
"private": true,
5+
"scripts": {
6+
"build": "rspress build",
7+
"dev": "rspress dev",
8+
"preview": "rspress preview"
9+
},
10+
"dependencies": {
11+
"react": "^18.3.1",
12+
"rspress": "workspace:*"
13+
},
14+
"devDependencies": {
15+
"@rspress/plugin-algolia": "workspace:*",
16+
"@types/node": "^18.11.17",
17+
"@types/react": "^18.3.18"
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import * as path from 'node:path';
2+
import { defineConfig } from 'rspress/config';
3+
4+
export default defineConfig({
5+
root: path.join(__dirname, 'doc'),
6+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Search as PluginAlgoliaSearch } from '@rspress/plugin-algolia/runtime';
2+
3+
const Search = () => {
4+
return (
5+
<PluginAlgoliaSearch
6+
docSearchProps={{
7+
appId: "R2IYF7ETH7",
8+
apiKey: "599cec31baffa4868cae4e79f180729b",
9+
indexName: "docsearch",
10+
}}
11+
/>
12+
);
13+
};
14+
export { Search }
15+
export * from 'rspress/theme'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"compilerOptions": {
3+
"moduleResolution": "bundler",
4+
"module": "ESNext",
5+
"jsx": "react-jsx"
6+
}
7+
}

e2e/tests/search-algolia.test.ts

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import assert from 'node:assert';
2+
import path from 'node:path';
3+
import { expect, test } from '@playwright/test';
4+
import { getPort, killProcess, runDevCommand } from '../utils/runCommands';
5+
6+
const fixtureDir = path.resolve(__dirname, '../fixtures');
7+
8+
test.describe('search code blocks test', async () => {
9+
let appPort;
10+
let app;
11+
12+
test.beforeAll(async () => {
13+
const appDir = path.join(fixtureDir, 'search-algolia');
14+
appPort = await getPort();
15+
app = await runDevCommand(appDir, appPort);
16+
});
17+
18+
test.afterAll(async () => {
19+
if (app) {
20+
await killProcess(app);
21+
}
22+
});
23+
24+
test('should search by algolia', async ({ page }) => {
25+
await page.goto(`http://localhost:${appPort}`);
26+
27+
const searchButton = await page.$('.DocSearch.DocSearch-Button');
28+
assert(searchButton);
29+
await searchButton.click();
30+
31+
const searchBar = await page.$('.DocSearch-SearchBar');
32+
expect(await searchBar?.isVisible()).toBeTruthy();
33+
});
34+
});

packages/document/docs/en/plugin/official-plugins/_meta.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@
99
"preview",
1010
"playground",
1111
"rss",
12-
"shiki"
12+
"shiki",
13+
"algolia"
1314
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,302 @@
1+
# @rspress/plugin-algolia
2+
3+
import { SourceCode } from 'rspress/theme';
4+
5+
<SourceCode href="https://github.com/web-infra-dev/rspress/tree/main/packages/plugin-algolia" />
6+
7+
Based on [docsearch](https://docsearch.algolia.com), this plugin replaces Rspress's built-in search with [algolia](https://www.algolia.com/).
8+
9+
## Installation
10+
11+
import { PackageManagerTabs } from '@theme';
12+
13+
<PackageManagerTabs command="add @rspress/plugin-algolia -D" />
14+
15+
## Usage
16+
17+
First, add the following configuration to your config file:
18+
19+
```ts
20+
// rspress.config.ts
21+
import path from 'path';
22+
import { defineConfig } from 'rspress';
23+
import { pluginAlgolia } from '@rspress/plugin-algolia';
24+
25+
export default defineConfig({
26+
plugins: [pluginAlgolia()],
27+
});
28+
```
29+
30+
Then override the `Search` component with an algolia-supported search box via [Custom Theme](/guide/advanced/custom-theme).
31+
32+
```tsx
33+
// theme/index.tsx
34+
import { Search as PluginAlgoliaSearch } from '@rspress/plugin-algolia/runtime';
35+
36+
const Search = () => {
37+
return (
38+
<PluginAlgoliaSearch
39+
docSearchProps={{
40+
appId: 'R2IYF7ETH7', // Replace with your own Algolia appId
41+
apiKey: '599cec31baffa4868cae4e79f180729b', // Replace with your own Algolia apiKey
42+
indexName: 'docsearch', // Replace with your own Algolia indexName
43+
}}
44+
/>
45+
);
46+
};
47+
export { Search };
48+
export * from 'rspress/theme';
49+
```
50+
51+
## Configuration
52+
53+
The plugin accepts an options object with the following type:
54+
55+
```ts
56+
interface Options {
57+
verificationContent?: string;
58+
}
59+
```
60+
61+
### verificationContent
62+
63+
- Type: `string | undefined`
64+
- Default: `undefined`
65+
66+
Used for meta tag verification when creating algolia crawler. Format: `<meta name="algolia-site-verification" content="YOUR_VERIFICATION_CONTENT" />`. Refer to [Create a new crawler - algolia](https://www.algolia.com/doc/tools/crawler/getting-started/create-crawler/#dns)
67+
68+
## SearchProps
69+
70+
The `SearchProps` type from `@rspress/plugin-algolia/runtime` is as follows:
71+
72+
```ts
73+
import type { DocSearchProps } from '@docsearch/react';
74+
75+
type Locales = Record<
76+
string,
77+
{ translations: DocSearchProps['translations']; placeholder: string }
78+
>;
79+
type SearchProps = {
80+
/**
81+
* @link https://docsearch.algolia.com/docs/api
82+
*/
83+
docSearchProps?: DocSearchProps;
84+
locales?: Locales;
85+
};
86+
```
87+
88+
### docSearchProps
89+
90+
- Type: `import('@docsearch/react').DocSearchProps`
91+
- Default: `undefined`
92+
93+
`docSearchProps` will be directly passed to the `<DocSearch />` component in `@docsearch/react`. For specific types, please refer to [docsearch documentation](https://docsearch.algolia.com/docs/api).
94+
95+
### locales
96+
97+
- Type:
98+
99+
```ts
100+
type Locales = Record<
101+
string,
102+
{ translations: DocSearchProps['translations']; placeholder: string }
103+
>;
104+
```
105+
106+
- Default: `{}`
107+
108+
For customizing translated text in different languages, Rspress provides the following translated text, which can be imported through import.
109+
110+
<details>
111+
112+
```ts
113+
export const ZH_LOCALES: Locales = {
114+
zh: {
115+
placeholder: '搜索文档',
116+
translations: {
117+
button: {
118+
buttonText: '搜索',
119+
buttonAriaLabel: '搜索',
120+
},
121+
modal: {
122+
searchBox: {
123+
resetButtonTitle: '清除查询条件',
124+
resetButtonAriaLabel: '清除查询条件',
125+
cancelButtonText: '取消',
126+
cancelButtonAriaLabel: '取消',
127+
},
128+
startScreen: {
129+
recentSearchesTitle: '搜索历史',
130+
noRecentSearchesText: '没有搜索历史',
131+
saveRecentSearchButtonTitle: '保存至搜索历史',
132+
removeRecentSearchButtonTitle: '从搜索历史中移除',
133+
favoriteSearchesTitle: '收藏',
134+
removeFavoriteSearchButtonTitle: '从收藏中移除',
135+
},
136+
errorScreen: {
137+
titleText: '无法获取结果',
138+
helpText: '你可能需要检查你的网络连接',
139+
},
140+
footer: {
141+
selectText: '选择',
142+
navigateText: '切换',
143+
closeText: '关闭',
144+
searchByText: '搜索提供者',
145+
},
146+
noResultsScreen: {
147+
noResultsText: '无法找到相关结果',
148+
suggestedQueryText: '你可以尝试查询',
149+
reportMissingResultsText: '你认为该查询应该有结果?',
150+
reportMissingResultsLinkText: '点击反馈',
151+
},
152+
},
153+
},
154+
},
155+
} as const;
156+
```
157+
158+
</details>
159+
160+
Rspress provides Chinese translation by default, and you can customize translated text in different languages ​​through `locales`.
161+
162+
- Example:
163+
164+
```tsx
165+
import { Search as PluginAlgoliaSearch, ZH_LOCALES } from '@rspress/plugin-algolia/runtime';
166+
167+
<PluginAlgoliaSearch locales={ZH_LOCALES} />
168+
// or
169+
<PluginAlgoliaSearch
170+
locales={{
171+
en: {
172+
placeholder: 'Search Documentation',
173+
translations: {
174+
button: {
175+
buttonText: 'Search',
176+
buttonAriaLabel: 'Search',
177+
}
178+
}
179+
},
180+
...ZH_LOCALES,
181+
}}
182+
/>
183+
```
184+
185+
## Algolia crawler config
186+
187+
Here is an example config based on what this site uses:
188+
189+
<details>
190+
191+
```tsx
192+
new Crawler({
193+
appId: 'YOUR_APP_ID',
194+
apiKey: 'YOUR_API_KEY',
195+
rateLimit: 8,
196+
maxDepth: 10,
197+
startUrls: ['https://rspress.dev'],
198+
sitemaps: ['https://rspress.dev/sitemap.xml'],
199+
discoveryPatterns: ['https://rspress.dev/**'],
200+
actions: [
201+
{
202+
indexName: 'doc_search_rspress_pages',
203+
pathsToMatch: ['https://rspress.dev/**'],
204+
recordExtractor: ({ $, helpers }) => {
205+
return helpers.docsearch({
206+
recordProps: {
207+
lvl0: {
208+
selectors: '',
209+
defaultValue: 'Documentation',
210+
},
211+
lvl1: '.rspress-doc h1',
212+
lvl2: '.rspress-doc h2',
213+
lvl3: '.rspress-doc h3',
214+
lvl4: '.rspress-doc h4',
215+
lvl5: '.rspress-doc h5',
216+
lvl6: '.rspress-doc pre > code', // if you want to search code blocks, add this line
217+
content: '.rspress-doc p, .rspress-doc li',
218+
},
219+
indexHeadings: true,
220+
aggregateContent: true,
221+
recordVersion: 'v3',
222+
});
223+
},
224+
},
225+
],
226+
initialIndexSettings: {
227+
doc_search_rspress_pages: {
228+
attributesForFaceting: ['type', 'lang'],
229+
attributesToRetrieve: ['hierarchy', 'content', 'anchor', 'url'],
230+
attributesToHighlight: ['hierarchy', 'content'],
231+
attributesToSnippet: ['content:10'],
232+
camelCaseAttributes: ['hierarchy', 'content'],
233+
searchableAttributes: [
234+
'unordered(hierarchy.lvl0)',
235+
'unordered(hierarchy.lvl1)',
236+
'unordered(hierarchy.lvl2)',
237+
'unordered(hierarchy.lvl3)',
238+
'unordered(hierarchy.lvl4)',
239+
'unordered(hierarchy.lvl5)',
240+
'unordered(hierarchy.lvl6)',
241+
'content',
242+
],
243+
distinct: true,
244+
attributeForDistinct: 'url',
245+
customRanking: [
246+
'desc(weight.pageRank)',
247+
'desc(weight.level)',
248+
'asc(weight.position)',
249+
],
250+
ranking: [
251+
'words',
252+
'filters',
253+
'typo',
254+
'attribute',
255+
'proximity',
256+
'exact',
257+
'custom',
258+
],
259+
minWordSizefor1Typo: 3,
260+
minWordSizefor2Typos: 7,
261+
allowTyposOnNumericTokens: false,
262+
minProximity: 1,
263+
ignorePlurals: true,
264+
advancedSyntax: true,
265+
attributeCriteriaComputedByMinProximity: true,
266+
removeWordsIfNoResults: 'allOptional',
267+
},
268+
},
269+
});
270+
```
271+
272+
</details>
273+
274+
## Distinguish search results based on i18n
275+
276+
You can achieve internationalized search results by combining [Runtime API](/api/client-api/api-runtime) with `docSearchProps`.
277+
278+
Here's an example using `docSearchProps.searchParameters`:
279+
280+
```tsx
281+
// theme/index.tsx
282+
import { useLang } from 'rspress/runtime';
283+
import { Search as PluginAlgoliaSearch } from '@rspress/plugin-algolia/runtime';
284+
285+
const Search = () => {
286+
const lang = useLang();
287+
return (
288+
<PluginAlgoliaSearch
289+
docSearchProps={{
290+
appId: 'R2IYF7ETH7',
291+
apiKey: '599cec31baffa4868cae4e79f180729b',
292+
indexName: 'docsearch',
293+
searchParameters: {
294+
facetFilters: [`language:${lang}`],
295+
},
296+
}}
297+
/>
298+
);
299+
};
300+
export { Search };
301+
export * from 'rspress/theme';
302+
```

0 commit comments

Comments
 (0)