rspress-plugin-typesense 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,21 +2,268 @@
2
2
 
3
3
  A plugin that brings lightning-fast, typo-tolerant search powered by Typesense to your Rspress site.
4
4
 
5
- ## Getting started
5
+ ## About Typesense & Rspress
6
+
7
+ [**Typesense**](https://typesense.org/) is an open-source, lightning-fast search engine that delivers instant, typo-tolerant results with minimal setup. It's an open source alternative to Algolia and an easier-to-use alternative to ElasticSearch.
6
8
 
7
- Install dependencies:
9
+ [**Rspress**](https://rspress.rs/) is a high-performance static site generator for documentation websites. Built with Rust, it offers a modern development experience and produces blazing-fast static sites.
10
+
11
+ Together, **Typesense** and **Rspress** provide a seamless way to add powerful, blazingly-fast search to modern documentation websites.
12
+
13
+ ## Installation
8
14
 
9
15
  ```bash
10
16
  npm install rspress-plugin-typesense
11
17
  ```
12
18
 
13
- ## About Typesense & Rspress
19
+ ## Usage
14
20
 
15
- [**Typesense**](https://typesense.org/) is an open-source, lightning-fast search engine that delivers instant, typo-tolerant results with minimal setup. It's an open source alternative to Algolia and an easier-to-use alternative to ElasticSearch.
21
+ ### 1. Start typesense server
16
22
 
17
- [**Rspress**](https://rspress.rs/) is a high-performance static site generator for documentation websites. Built with Rust, it offers a modern development experience and produces blazing-fast static sites.
23
+ You can either self-host the Typesense server or use [Typesense Cloud service](https://cloud.typesense.org/). Follow this [getting started guide](https://typesense.org/docs/guide/install-typesense.html) to set up your server and obtain the API key and server URL.
18
24
 
19
- Together, **Typesense** and **Rspress** provide a seamless way to add powerful, blazingly-fast search to modern documentation websites.
25
+ ### 2. Configure the plugin
26
+
27
+ First, add the plugin to your `rspress.config.ts`. You must provide your Typesense server details and an API key with **write permissions** so the plugin can create collections and index your documents during the build.
28
+
29
+ ```ts
30
+ // rspress.config.ts
31
+ import { defineConfig } from '@rspress/core';
32
+ import { pluginTypesense } from 'rspress-plugin-typesense';
33
+
34
+ export default defineConfig({
35
+ plugins: [
36
+ pluginTypesense({
37
+ collectionName: 'my_docs',
38
+ serverConfig: {
39
+ nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }],
40
+ apiKey: 'YOUR_TYPESENSE_ADMIN_API_KEY', // Requires Write permissions
41
+ },
42
+ }),
43
+ ],
44
+ });
45
+ ```
46
+
47
+ ### 3. Override the search component
48
+
49
+ Next, override Rspress's default `Search` component via a [Custom Theme](https://rspress.rs/guide/basic/custom-theme).
50
+
51
+ Provide a **Search-Only API Key** here. For security reasons, **never expose your Admin API Key in the frontend**.
52
+
53
+ ```tsx
54
+ // theme/index.tsx
55
+ import { Search as PluginTypesenseSearch } from 'rspress-plugin-typesense/runtime';
56
+
57
+ const Search = () => {
58
+ return (
59
+ <PluginTypesenseSearch
60
+ docSearchProps={{
61
+ typesenseServerConfig: {
62
+ nodes: [{ url: 'YOUR_TYPESENSE_SERVER_URL' }],
63
+ apiKey: 'YOUR_TYPESENSE_SEARCH_ONLY_API_KEY', // Safe for browsers
64
+ },
65
+ }}
66
+ />
67
+ );
68
+ };
69
+
70
+ export { Search };
71
+ export * from '@rspress/core/theme-original';
72
+ ```
73
+
74
+ ### 4. Build and index
75
+
76
+ Run the build command to generate your site and index your content into Typesense.
77
+
78
+ <PackageManagerTabs command="run build" />
79
+
80
+ All set! You've successfully integrated typo-tolerant search into your documentation. The plugin automatically handles filtering based on the user's current language and version.
81
+
82
+ ## Plugin configuration (Backend)
83
+
84
+ The `pluginTypesense` function accepts an options object with the following properties:
85
+
86
+ ```ts
87
+ export interface TypesensePluginOptions {
88
+ /**
89
+ * Typesense server connection options.
90
+ * The API key must have write permissions to create and index collections.
91
+ */
92
+ serverConfig: ConfigurationOptions;
93
+
94
+ /**
95
+ * The base name of the Typesense collection.
96
+ * Note: The plugin creates dedicated localized collections (e.g., `my_docs_en`).
97
+ */
98
+ collectionName: string;
99
+
100
+ /**
101
+ * Optional schema overrides. Can be a global settings object or a map keyed by language.
102
+ */
103
+ customCollectionSettings?: CustomCollectionSettingsConfig;
104
+
105
+ /**
106
+ * Whether to index code blocks into Typesense.
107
+ * Defaults to `false` to avoid search noise and bloated index sizes.
108
+ */
109
+ indexCodeBlocks?: boolean;
110
+
111
+ /**
112
+ * Whether a failed indexing attempt should crash the build process.
113
+ * Defaults to `true`.
114
+ */
115
+ failOnIndexError?: boolean;
116
+
117
+ /**
118
+ * Whether to automatically filter search results by the active documentation version.
119
+ * Defaults to `true`.
120
+ */
121
+ versionedSearch?: boolean;
122
+ }
123
+ ```
124
+
125
+ ### customCollectionSettings
126
+
127
+ - **Type**:
128
+
129
+ ```ts
130
+ type CustomCollectionSettingsConfig =
131
+ | CustomCollectionSettings
132
+ | Record<string, CustomCollectionSettings>;
133
+ ```
134
+
135
+ - **Default**: `undefined`
136
+
137
+ Allows you to override the Typesense schema configuration. You can pass a single global configuration, or a map of configurations keyed by language (useful if you need specific token separators for languages like Chinese or Japanese).
138
+
139
+ #### Customizing schema and injecting custom data
140
+
141
+ For advanced use cases like adding custom tags for faceted search or boosting the search ranking of specific pages, you can extend the default schema and mutate records before they are indexed.
142
+
143
+ To do this:
144
+
145
+ 1. Use the `getDefaultCollectionFields` helper in `customCollectionSettings` to safely append new fields to the collection schema.
146
+ 2. Use the `transformRecord` hook to populate those fields or modify existing weights based on the `route`.
147
+
148
+ Here is an example showing how to add a custom `category` field for filtering, and how to boost the `page_rank` of "Getting Started" guides:
149
+
150
+ ```ts
151
+ import {
152
+ pluginTypesense,
153
+ getDefaultCollectionFields,
154
+ } from 'rspress-plugin-typesense';
155
+
156
+ pluginTypesense({
157
+ collectionName: 'my_docs',
158
+ serverConfig: {
159
+ /* ... */
160
+ },
161
+
162
+ // 1. Extend the schema to add a custom 'category' field
163
+ customCollectionSettings: {
164
+ en: {
165
+ fields: (params) => [
166
+ ...getDefaultCollectionFields(params),
167
+ { name: 'category', type: 'string', facet: true, optional: true },
168
+ ],
169
+ },
170
+ },
171
+
172
+ // 2. Mutate the record before it gets indexed
173
+ transformRecord(record, route) {
174
+ // Example: Inject a custom tag for faceted search
175
+ if (route.routePath.startsWith('/api/')) {
176
+ record.category = 'API Reference';
177
+ }
178
+
179
+ // Example: Boost the search priority of important pages
180
+ if (route.routePath.includes('getting-started')) {
181
+ record.weight.page_rank = 100; // Default is 0
182
+ }
183
+
184
+ return record;
185
+ },
186
+ });
187
+ ```
188
+
189
+ In the frontend, you could now pass `typesenseSearchParams: { filter_by: 'category:=API Reference' }` to your `<Search />` component to restrict results.
190
+
191
+ ### indexCodeBlocks
192
+
193
+ - **Type**: `boolean`
194
+ - **Default**: `false`
195
+
196
+ By default, the plugin only indexes headers (`h1-h6`), paragraphs, lists and tables. Enabling this will also extract text from inside code blocks.
197
+
198
+ ### failOnIndexError
199
+
200
+ - **Type**: `boolean`
201
+ - **Default**: `true`
202
+
203
+ By default, if the Typesense indexing step fails (e.g., network timeout), the build process will crash. Set this to `false` if you want your CI/CD deployments to succeed even if search indexing fails.
204
+
205
+ ### versionedSearch
206
+
207
+ - **Type**: `boolean`
208
+ - **Default**: `true`
209
+
210
+ If your Rspress site utilizes multiple versions, the plugin tags every indexed document with its respective version and exposes this setting to the frontend via a virtual module. This ensures users only see search results relevant to the documentation version they are currently viewing. Set this to `false` if you want to search across all versions.
211
+
212
+ ## Search component props (Frontend)
213
+
214
+ The `<PluginTypesenseSearch />` component accepts the following properties:
215
+
216
+ ```ts
217
+ type SearchProps = {
218
+ docSearchProps: TypesenseDocSearchProps;
219
+ locales?: Locales;
220
+ };
221
+ ```
222
+
223
+ ### `docSearchProps`
224
+
225
+ - **Type**: `TypesenseDocSearchProps`
226
+ - **Required**: Yes (`typesenseServerConfig` must be provided)
227
+
228
+ Parameters passed directly to the underlying `typesense-docsearch-react` modal.
229
+
230
+ _You do not need to provide `typesenseCollectionName`. The plugin automatically injects the collection name via a virtual module._
231
+
232
+ ### `locales`
233
+
234
+ - **Type**:
235
+
236
+ ```ts
237
+ type Locales = Record<
238
+ string,
239
+ { translations: DocSearchProps['translations']; placeholder: string }
240
+ >;
241
+ ```
242
+
243
+ - **Default**: `{}`
244
+
245
+ Allows you to customize the placeholder and modal translations based on the active language. You can see the list of translations provided by the plugin [here](/src/runtime/locales.ts).
246
+
247
+ **Example:**
248
+
249
+ ```tsx
250
+ import { Search as PluginTypesenseSearch } from 'rspress-plugin-typesense/runtime';
251
+
252
+ <PluginTypesenseSearch
253
+ locales={{
254
+ en: {
255
+ placeholder: 'Search documentation',
256
+ translations: {
257
+ button: {
258
+ buttonText: 'Search',
259
+ buttonAriaLabel: 'Search',
260
+ },
261
+ },
262
+ },
263
+ ...ZH_LOCALES,
264
+ }}
265
+ />;
266
+ ```
20
267
 
21
268
  ## License
22
269
 
package/dist/index.d.ts CHANGED
@@ -1,12 +1,49 @@
1
- import type { RspressPlugin } from '@rspress/core';
1
+ import type { RouteMeta, RspressPlugin } from '@rspress/core';
2
2
  import type { ConfigurationOptions } from 'typesense/lib/Typesense/Configuration';
3
- import type { CustomCollectionSettingsConfig } from './types.js';
3
+ import type { CustomCollectionSettingsConfig, DocSearchRecord } from './types.js';
4
4
  import { getDefaultCollectionFields } from './typesenseHelper.js';
5
- export type { CustomCollectionSettings, CustomCollectionSettingsConfig, } from './types.js';
5
+ export type { CustomCollectionSettings, CustomCollectionSettingsConfig, DocSearchRecord, } from './types.js';
6
6
  export { getDefaultCollectionFields };
7
+ /**
8
+ * Options for the Typesense plugin.
9
+ *
10
+ * The server configuration requires an API key with **write permissions**,
11
+ * as the plugin creates and manages collections during indexing.
12
+ */
7
13
  export interface TypesensePluginOptions {
8
- typesenseOptions: ConfigurationOptions;
14
+ /**
15
+ * Typesense server connection options.
16
+ * The API key must have write permissions to create and index collections.
17
+ */
18
+ serverConfig: ConfigurationOptions;
19
+ /**
20
+ * The base name of the Typesense collection.
21
+ * Note: The plugin creates dedicated localized collections (e.g., `my_docs_en`).
22
+ */
9
23
  collectionName: string;
24
+ /**
25
+ * Optional schema overrides. Can be a global settings object or a map keyed by language.
26
+ */
10
27
  customCollectionSettings?: CustomCollectionSettingsConfig;
28
+ /**
29
+ * Whether to index code blocks into Typesense.
30
+ * Defaults to `false` to avoid search noise and bloated index sizes.
31
+ */
32
+ indexCodeBlocks?: boolean;
33
+ /**
34
+ * Whether a failed indexing attempt should crash the build process.
35
+ * Defaults to `true`.
36
+ */
37
+ failOnIndexError?: boolean;
38
+ /**
39
+ * Whether to automatically filter search results by the active documentation version.
40
+ * Defaults to `true`.
41
+ */
42
+ versionedSearch?: boolean;
43
+ /**
44
+ * Hook to mutate or enrich the record before it gets indexed.
45
+ * Useful for attaching custom fields or tags.
46
+ */
47
+ transformRecord?: (record: DocSearchRecord, route: RouteMeta) => DocSearchRecord;
11
48
  }
12
49
  export declare function pluginTypesense(options: TypesensePluginOptions): RspressPlugin;
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import { TypesenseHelper, getDefaultCollectionFields } from "./typesenseHelper.js";
4
4
  import { IndexFromHtml } from "./indexFromHtml.js";
5
+ import { ImportError } from "typesense/lib/Typesense/Errors";
5
6
  function pluginTypesense(options) {
6
7
  let generatedRoutes = [];
7
8
  return {
@@ -12,11 +13,20 @@ function pluginTypesense(options) {
12
13
  async routeGenerated (routes) {
13
14
  generatedRoutes = routes;
14
15
  },
16
+ async addRuntimeModules () {
17
+ const configPayload = {
18
+ collectionName: options.collectionName,
19
+ versionedSearch: options.versionedSearch ?? true
20
+ };
21
+ return {
22
+ 'virtual-typesense-config': `export default ${JSON.stringify(configPayload)};`
23
+ };
24
+ },
15
25
  async afterBuild (config, isProd) {
16
26
  if (!isProd) return;
17
27
  const outDir = config.outDir || 'doc_build';
18
28
  const defaultLang = config.lang || 'en';
19
- const isVersioned = config.search?.versioned ?? true;
29
+ const isVersioned = options.versionedSearch ?? true;
20
30
  const defaultVersion = config.multiVersion?.default || '';
21
31
  if (0 === generatedRoutes.length) return void console.warn(`\n\x1b[33m⚠ [TypesensePlugin] No routes generated.\x1b[0m \x1b[90mSkipping indexing.\x1b[0m\n`);
22
32
  const routeGroups = {};
@@ -37,7 +47,9 @@ function pluginTypesense(options) {
37
47
  routeGroups[groupKey].routes.push(route);
38
48
  }
39
49
  if (0 === Object.keys(routeGroups).length) return void console.warn(`\n\x1b[33m⚠ [TypesensePlugin] No routes found for indexing.\x1b[0m \x1b[90mSkipping indexing process.\x1b[0m\n`);
40
- const extractor = new IndexFromHtml();
50
+ const extractor = new IndexFromHtml({
51
+ indexCodeBlocks: options.indexCodeBlocks ?? false
52
+ });
41
53
  for(const groupKey in routeGroups){
42
54
  const { locale, routes } = routeGroups[groupKey];
43
55
  const aliasName = `${options.collectionName}_${locale}`;
@@ -47,15 +59,23 @@ function pluginTypesense(options) {
47
59
  console.log(`\n\x1b[1m\x1b[36m[TypesensePlugin]\x1b[0m \x1b[1mProcessing group:\x1b[0m \x1b[35m${aliasName}\x1b[0m \x1b[90m(${routes.length} routes)\x1b[0m`);
48
60
  const localizedCustomSettings = resolveCustomSettings(options.customCollectionSettings, locale);
49
61
  const helper = new TypesenseHelper({
50
- config: options.typesenseOptions,
62
+ config: options.serverConfig,
51
63
  aliasName,
52
64
  collectionNameTmp,
53
65
  customSettings: localizedCustomSettings,
54
66
  locale,
55
67
  isVersioned
56
68
  });
57
- await helper.init();
58
- await helper.createTmpCollection();
69
+ try {
70
+ await helper.init();
71
+ await helper.createTmpCollection();
72
+ } catch (error) {
73
+ console.error(`\n\x1b[31m✖ [TypesensePlugin] Failed to initialize/create collection:\x1b[0m \x1b[37m${collectionNameTmp}\x1b[0m`);
74
+ console.error(` \x1b[31m↳ ${error instanceof Error ? error.message : error}\x1b[0m\n`);
75
+ if (false !== options.failOnIndexError) throw error;
76
+ console.warn("\x1b[33m⚠ [TypesensePlugin] Skipping group indexing due to failOnIndexError=false.\x1b[0m");
77
+ continue;
78
+ }
59
79
  let totalRecords = 0;
60
80
  for (const route of routes){
61
81
  const version = route.version || defaultVersion;
@@ -79,22 +99,30 @@ function pluginTypesense(options) {
79
99
  }
80
100
  try {
81
101
  const htmlContent = fs.readFileSync(htmlPath, 'utf-8');
82
- const records = extractor.getRecords(htmlContent, route.routePath, locale);
102
+ let records = extractor.getRecords(htmlContent, route.routePath, locale);
83
103
  if (isVersioned && version) records.forEach((r)=>r.version = version);
104
+ if (options.transformRecord) records = records.map((record)=>options.transformRecord(record, route));
84
105
  if (records.length > 0) totalRecords += await helper.addRecords(records, route.routePath, false, padLength);
85
106
  } catch (error) {
86
107
  const fillerLength = Math.max(2, padLength - route.routePath.length);
87
108
  const filler = '\x1b[90m' + '.'.repeat(fillerLength) + '\x1b[0m';
88
109
  console.error(` \x1b[31m✖\x1b[0m \x1b[37m${route.routePath}\x1b[0m ${filler} \x1b[31m failed\x1b[0m \x1b[90m(Processing error)\x1b[0m`);
89
- console.error(` \x1b[31m↳ ${error instanceof Error ? error.message : error}\x1b[0m`);
110
+ if (error instanceof ImportError) {
111
+ console.error(" \x1b[31m↳ Import error\x1b[0m");
112
+ console.error(error.importResults);
113
+ if (false !== options.failOnIndexError) throw error;
114
+ console.warn("\x1b[33m⚠ [TypesensePlugin] Skipping failure due to failOnIndexError=false.\x1b[0m");
115
+ } else console.error(` \x1b[31m↳ ${error instanceof Error ? error.message : error}\x1b[0m`);
90
116
  }
91
117
  }
92
118
  try {
93
119
  await helper.commitTmpCollection();
94
120
  console.log(`\x1b[32m✔ Indexing complete for\x1b[0m \x1b[35m${aliasName}\x1b[0m \x1b[90m—\x1b[0m \x1b[33m${totalRecords}\x1b[0m \x1b[90mtotal records!\x1b[0m`);
95
121
  } catch (error) {
96
- console.error(`[TypesensePlugin] Failed to commit collection ${aliasName}:`, error);
97
- throw error;
122
+ console.error(`\n\x1b[31m✖ [TypesensePlugin] Failed to commit collection:\x1b[0m \x1b[37m${aliasName}\x1b[0m`);
123
+ console.error(` \x1b[31m↳ ${error instanceof Error ? error.message : error}\x1b[0m\n`);
124
+ if (false !== options.failOnIndexError) throw error;
125
+ console.warn("\x1b[33m⚠ [TypesensePlugin] Skipping failure due to failOnIndexError=false.\x1b[0m");
98
126
  }
99
127
  }
100
128
  }
@@ -102,7 +130,7 @@ function pluginTypesense(options) {
102
130
  }
103
131
  function resolveCustomSettings(settings, locale) {
104
132
  if (!settings) return null;
105
- const isGlobalConfig = 'token_separators' in settings || 'symbols_to_index' in settings || 'field_definitions' in settings || 'enable_nested_fields' in settings;
133
+ const isGlobalConfig = 'token_separators' in settings || 'symbols_to_index' in settings || 'fields' in settings || 'enable_nested_fields' in settings;
106
134
  if (isGlobalConfig) return settings;
107
135
  const perLangSettings = settings;
108
136
  return perLangSettings[locale] || null;
@@ -2,6 +2,9 @@ import type { DocSearchRecord } from './types.js';
2
2
  export declare class IndexFromHtml {
3
3
  private levels;
4
4
  private selectors;
5
+ constructor(options?: {
6
+ indexCodeBlocks?: boolean;
7
+ });
5
8
  getRecords(html: string, url: string, lang?: string): DocSearchRecord[];
6
9
  private getLevelFromTag;
7
10
  private generateEmptyHierarchy;
@@ -10,42 +10,50 @@ class IndexFromHtml {
10
10
  'lvl5',
11
11
  'lvl6'
12
12
  ];
13
- selectors = {
14
- lvl0: {
15
- selector: '.rp-nav-menu__item--active',
16
- global: true
17
- },
18
- lvl1: {
19
- selector: '.rp-doc h1',
20
- global: false
21
- },
22
- lvl2: {
23
- selector: '.rp-doc h2',
24
- global: false
25
- },
26
- lvl3: {
27
- selector: '.rp-doc h3',
28
- global: false
29
- },
30
- lvl4: {
31
- selector: '.rp-doc h4',
32
- global: false
33
- },
34
- lvl5: {
35
- selector: '.rp-doc h5',
36
- global: false
37
- },
38
- lvl6: {
39
- selector: '.rp-doc h6',
40
- global: false
41
- },
42
- content: {
43
- selector: '.rp-doc p, .rp-doc li, .rp-doc table, .rp-doc .rp-callout, .rp-doc .rp-codeblock__content .rp-codeblock__content__scroll-container',
44
- global: false
45
- }
46
- };
13
+ selectors;
14
+ constructor(options){
15
+ const docClass = '.rspress-doc';
16
+ let contentSelector = `${docClass} p, ${docClass} li, ${docClass} td, ${docClass} th`;
17
+ if (options?.indexCodeBlocks) contentSelector += `, ${docClass} pre > code`;
18
+ this.selectors = {
19
+ lvl0: {
20
+ selector: '.rp-nav-menu__item--active',
21
+ global: true
22
+ },
23
+ lvl1: {
24
+ selector: `${docClass} h1`,
25
+ global: false
26
+ },
27
+ lvl2: {
28
+ selector: `${docClass} h2`,
29
+ global: false
30
+ },
31
+ lvl3: {
32
+ selector: `${docClass} h3`,
33
+ global: false
34
+ },
35
+ lvl4: {
36
+ selector: `${docClass} h4`,
37
+ global: false
38
+ },
39
+ lvl5: {
40
+ selector: `${docClass} h5`,
41
+ global: false
42
+ },
43
+ lvl6: {
44
+ selector: `${docClass} h6`,
45
+ global: false
46
+ },
47
+ content: {
48
+ selector: contentSelector,
49
+ global: false
50
+ }
51
+ };
52
+ }
47
53
  getRecords(html, url, lang) {
48
54
  const $ = __rspack_external_cheerio.load(html);
55
+ $('.rp-badge').remove();
56
+ $('.rp-not-doc').remove();
49
57
  const records = [];
50
58
  const getGlobalText = (selector)=>{
51
59
  const el = $(selector).first();
@@ -92,7 +100,7 @@ class IndexFromHtml {
92
100
  let levelWeight;
93
101
  if ('content' !== currentLevel) levelWeight = 100 - 10 * currentLevelInt;
94
102
  else {
95
- const isCodeBlock = el.hasClass('rp-codeblock__content__scroll-container') || el.parents('.rp-codeblock, pre').length > 0;
103
+ const isCodeBlock = 'code' === tagName || el.parents('pre').length > 0;
96
104
  levelWeight = isCodeBlock ? 0 : 10;
97
105
  }
98
106
  const weight = {
@@ -1,10 +1,12 @@
1
1
  import type { DocSearchProps } from 'typesense-docsearch-react';
2
2
  import type { Locales } from './locales.js';
3
+ export type TypesenseDocSearchProps = Omit<DocSearchProps, 'translations' | 'typesenseCollectionName' | 'typesenseSearchParameters'> & {
4
+ typesenseSearchParameters?: DocSearchProps['typesenseSearchParameters'];
5
+ };
3
6
  type SearchProps = {
4
- docSearchProps: Omit<DocSearchProps, 'translations'>;
7
+ docSearchProps: TypesenseDocSearchProps;
5
8
  locales?: Locales;
6
- versionedSearch?: boolean;
7
9
  };
8
- declare function Search({ locales, versionedSearch, docSearchProps: { typesenseCollectionName, transformItems, typesenseSearchParameters, ...docSearchProps }, }: SearchProps): import("react/jsx-runtime").JSX.Element;
10
+ declare function Search({ locales, docSearchProps: { transformItems, typesenseSearchParameters, ...docSearchProps }, }: SearchProps): import("react/jsx-runtime").JSX.Element;
9
11
  export type { SearchProps };
10
12
  export default Search;
@@ -2,6 +2,7 @@ import { Fragment, jsx } from "react/jsx-runtime";
2
2
  import { DocSearch } from "typesense-docsearch-react";
3
3
  import { useLang, useNavigate, useVersion } from "@rspress/core/runtime";
4
4
  import { Link } from "@theme";
5
+ import virtual_typesense_config from "virtual-typesense-config";
5
6
  const Hit = ({ hit, children })=>/*#__PURE__*/ jsx(Link, {
6
7
  href: hit.url,
7
8
  children: children
@@ -10,18 +11,18 @@ const safeEscapeHighlights = (str)=>{
10
11
  if (!str) return str;
11
12
  return str.replace(/<(?!mark>|\/mark>)/gi, '&lt;').replace(/(?<!<mark|<\/mark)>/gi, '&gt;');
12
13
  };
13
- function Search({ locales = {}, versionedSearch = true, docSearchProps: { typesenseCollectionName, transformItems, typesenseSearchParameters, ...docSearchProps } }) {
14
+ function Search({ locales = {}, docSearchProps: { transformItems, typesenseSearchParameters, ...docSearchProps } }) {
14
15
  const navigate = useNavigate();
15
16
  const version = useVersion();
16
17
  const lang = useLang() || 'en';
17
18
  const { translations, placeholder } = locales?.[lang] ?? {};
18
- const resolvedCollectionName = `${typesenseCollectionName}_${lang}`;
19
+ const resolvedCollectionName = `${virtual_typesense_config.collectionName}_${lang}`;
19
20
  const searchParams = {
20
21
  ...typesenseSearchParameters || {}
21
22
  };
22
23
  const filters = [];
23
24
  if (searchParams.filter_by) filters.push(`(${searchParams.filter_by})`);
24
- if (versionedSearch && version) filters.push(`version:=\`${version}\``);
25
+ if (virtual_typesense_config.versionedSearch && version) filters.push(`version:=\`${version}\``);
25
26
  if (filters.length > 0) searchParams.filter_by = filters.join(' && ');
26
27
  return /*#__PURE__*/ jsx(Fragment, {
27
28
  children: /*#__PURE__*/ jsx(DocSearch, {
@@ -1,2 +1,3 @@
1
- export { type Locales, RU_LOCALES, ZH_LOCALES } from './locales.js';
1
+ export { type Locales, RU_LOCALES, ZH_LOCALES, VN_LOCALES } from './locales.js';
2
2
  export { Search, type SearchProps } from './Search.js';
3
+ export type { TypesenseDocSearchProps } from './RealSearch.js';
@@ -1,2 +1,2 @@
1
- export { RU_LOCALES, ZH_LOCALES } from "./locales.js";
1
+ export { RU_LOCALES, VN_LOCALES, ZH_LOCALES } from "./locales.js";
2
2
  export { Search } from "./Search.js";
@@ -5,3 +5,4 @@ export type Locales = Record<string, {
5
5
  }>;
6
6
  export declare const ZH_LOCALES: Locales;
7
7
  export declare const RU_LOCALES: Locales;
8
+ export declare const VN_LOCALES: Locales;
@@ -84,4 +84,47 @@ const RU_LOCALES = {
84
84
  }
85
85
  }
86
86
  };
87
- export { RU_LOCALES, ZH_LOCALES };
87
+ const VN_LOCALES = {
88
+ vn: {
89
+ placeholder: 'Tìm kiếm tài liệu',
90
+ translations: {
91
+ button: {
92
+ buttonText: 'Tìm kiếm',
93
+ buttonAriaLabel: 'Tìm kiếm'
94
+ },
95
+ modal: {
96
+ searchBox: {
97
+ resetButtonTitle: 'Xóa truy vấn',
98
+ resetButtonAriaLabel: 'Xóa truy vấn',
99
+ cancelButtonText: 'Hủy',
100
+ cancelButtonAriaLabel: 'Hủy'
101
+ },
102
+ startScreen: {
103
+ recentSearchesTitle: 'Gần đây',
104
+ noRecentSearchesText: 'Chưa có tìm kiếm gần đây',
105
+ saveRecentSearchButtonTitle: 'Lưu tìm kiếm này',
106
+ removeRecentSearchButtonTitle: 'Xóa tìm kiếm này khỏi lịch sử',
107
+ favoriteSearchesTitle: 'Yêu thích',
108
+ removeFavoriteSearchButtonTitle: 'Xóa tìm kiếm này khỏi mục yêu thích'
109
+ },
110
+ errorScreen: {
111
+ titleText: 'Không thể tải kết quả',
112
+ helpText: 'Hãy kiểm tra lại kết nối mạng của bạn.'
113
+ },
114
+ footer: {
115
+ selectText: 'để chọn',
116
+ navigateText: 'để di chuyển',
117
+ closeText: 'để đóng',
118
+ searchByText: 'Vận hành bởi'
119
+ },
120
+ noResultsScreen: {
121
+ noResultsText: 'Không có kết quả cho',
122
+ suggestedQueryText: 'Hãy thử tìm với từ khóa',
123
+ reportMissingResultsText: 'Bạn nghĩ truy vấn này nên có kết quả?',
124
+ reportMissingResultsLinkText: 'Hãy cho chúng tôi biết.'
125
+ }
126
+ }
127
+ }
128
+ }
129
+ };
130
+ export { RU_LOCALES, VN_LOCALES, ZH_LOCALES };
package/dist/types.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { CollectionFieldSchema } from 'typesense/lib/Typesense/Collection';
1
2
  import type { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
2
3
  export interface Hierarchy {
3
4
  [key: string]: string | null | undefined;
@@ -36,7 +37,8 @@ export interface FieldsParams {
36
37
  export interface CustomCollectionSettings {
37
38
  token_separators?: CollectionCreateSchema['token_separators'];
38
39
  symbols_to_index?: CollectionCreateSchema['symbols_to_index'];
39
- fields?: (params: FieldsParams) => CollectionCreateSchema['fields'];
40
+ fields?: (params: FieldsParams) => CollectionFieldSchema[];
40
41
  enable_nested_fields?: CollectionCreateSchema['enable_nested_fields'];
41
42
  }
43
+ /** Allows a single global config OR a map of configs keyed by language (e.g. `{ en: {...}, zh: {...} }`) */
42
44
  export type CustomCollectionSettingsConfig = CustomCollectionSettings | Record<string, CustomCollectionSettings>;
@@ -1,6 +1,6 @@
1
- import type { CollectionCreateSchema } from 'typesense/lib/Typesense/Collections';
2
1
  import type { ConfigurationOptions } from 'typesense/lib/Typesense/Configuration';
3
2
  import type { DocSearchRecord, CustomCollectionSettings, FieldsParams } from './types.js';
3
+ import { CollectionFieldSchema } from 'typesense/lib/Typesense/Collection';
4
4
  export interface TypesenseHelperOptions {
5
5
  config: ConfigurationOptions;
6
6
  aliasName: string;
@@ -47,4 +47,4 @@ export declare class TypesenseHelper {
47
47
  * },
48
48
  * }
49
49
  */
50
- export declare function getDefaultCollectionFields({ locale, isVersioned, }: FieldsParams): CollectionCreateSchema['fields'];
50
+ export declare function getDefaultCollectionFields({ locale, isVersioned, }: FieldsParams): CollectionFieldSchema[];
@@ -107,7 +107,7 @@ class TypesenseHelper {
107
107
  if (record.hierarchy && null != record.hierarchy[lvlKey]) transformedRecord[`hierarchy.lvl${x}`] = record.hierarchy[lvlKey];
108
108
  if (record.hierarchy_radio && null != record.hierarchy_radio[lvlKey]) transformedRecord[`hierarchy_radio.lvl${x}`] = record.hierarchy_radio[lvlKey];
109
109
  }
110
- if (isVersioned && record.version && 'string' == typeof record.version) transformedRecord['version'] = record.version.split(',');
110
+ if (isVersioned && record.version && 'string' == typeof record.version) transformedRecord['version'] = record.version;
111
111
  else if (!isVersioned) delete transformedRecord['version'];
112
112
  return transformedRecord;
113
113
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "rspress-plugin-typesense",
3
- "description": "A plugin to add Typesense-powered search to your Rspress site",
4
- "version": "0.0.1",
3
+ "description": "A plugin that adds lightning-fast, typo-tolerant, Typesense-powered search to your Rspress site",
4
+ "version": "0.0.3",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": {
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "cheerio": "^1.2.0",
35
+ "typesense": "^3.0.6",
35
36
  "typesense-docsearch-css": "^0.4.1",
36
37
  "typesense-docsearch-react": "^3.4.1"
37
38
  },