Skip to content

Commit deb376e

Browse files
committed
feat(theme-algolia): add option.replaceSearchResultPathname to process/replaceAll search result urls
#8428
1 parent e5b0707 commit deb376e

18 files changed

Lines changed: 214 additions & 51 deletions

File tree

packages/docusaurus-plugin-content-blog/package.json

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,6 @@
3535
"utility-types": "^3.10.0",
3636
"webpack": "^5.73.0"
3737
},
38-
"devDependencies": {
39-
"escape-string-regexp": "^4.0.0"
40-
},
4138
"peerDependencies": {
4239
"react": "^16.8.4 || ^17.0.0",
4340
"react-dom": "^16.8.4 || ^17.0.0"

packages/docusaurus-plugin-content-blog/src/__tests__/frontMatter.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import escapeStringRegexp from 'escape-string-regexp';
8+
import {escapeRegexp} from '@docusaurus/utils';
99
import {validateBlogPostFrontMatter} from '../frontMatter';
1010
import type {BlogPostFrontMatter} from '@docusaurus/plugin-content-blog';
1111

@@ -57,7 +57,7 @@ function testField(params: {
5757
} catch (err) {
5858
// eslint-disable-next-line jest/no-conditional-expect
5959
expect((err as Error).message).toMatch(
60-
new RegExp(escapeStringRegexp(message)),
60+
new RegExp(escapeRegexp(message)),
6161
);
6262
}
6363
});

packages/docusaurus-plugin-content-docs/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,6 @@
5656
"@types/js-yaml": "^4.0.5",
5757
"@types/picomatch": "^2.3.0",
5858
"commander": "^5.1.0",
59-
"escape-string-regexp": "^4.0.0",
6059
"picomatch": "^2.3.1",
6160
"shelljs": "^0.8.5"
6261
},

packages/docusaurus-plugin-content-docs/src/__tests__/frontMatter.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import escapeStringRegexp from 'escape-string-regexp';
8+
import {escapeRegexp} from '@docusaurus/utils';
99
import {validateDocFrontMatter} from '../frontMatter';
1010
import type {DocFrontMatter} from '@docusaurus/plugin-content-docs';
1111

@@ -57,7 +57,7 @@ function testField(params: {
5757
} catch (err) {
5858
// eslint-disable-next-line jest/no-conditional-expect
5959
expect((err as Error).message).toMatch(
60-
new RegExp(escapeStringRegexp(message)),
60+
new RegExp(escapeRegexp(message)),
6161
);
6262
}
6363
});

packages/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import {validateThemeConfig, DEFAULT_CONFIG} from '../validateThemeConfig';
8+
import {DEFAULT_CONFIG, validateThemeConfig} from '../validateThemeConfig';
99
import type {Joi} from '@docusaurus/utils-validation';
1010

1111
function testValidateThemeConfig(themeConfig: {[key: string]: unknown}) {
@@ -121,6 +121,53 @@ describe('validateThemeConfig', () => {
121121
});
122122
});
123123

124+
describe('replaceSearchResultPathname', () => {
125+
it('escapes from string', () => {
126+
const algolia = {
127+
appId: 'BH4D9OD16A',
128+
indexName: 'index',
129+
apiKey: 'apiKey',
130+
replaceSearchResultPathname: {
131+
from: '/docs/some-\\special-.[regexp]{chars*}',
132+
to: '/abc',
133+
},
134+
};
135+
expect(testValidateThemeConfig({algolia})).toEqual({
136+
algolia: {
137+
...DEFAULT_CONFIG,
138+
...algolia,
139+
replaceSearchResultPathname: {
140+
from: '/docs/some\\x2d\\\\special\\x2d\\.\\[regexp\\]\\{chars\\*\\}',
141+
to: '/abc',
142+
},
143+
},
144+
});
145+
});
146+
147+
it('converts from regexp to string', () => {
148+
const algolia = {
149+
appId: 'BH4D9OD16A',
150+
indexName: 'index',
151+
apiKey: 'apiKey',
152+
replaceSearchResultPathname: {
153+
from: /^\/docs\/(?:1\.0|next)/,
154+
to: '/abc',
155+
},
156+
};
157+
158+
expect(testValidateThemeConfig({algolia})).toEqual({
159+
algolia: {
160+
...DEFAULT_CONFIG,
161+
...algolia,
162+
replaceSearchResultPathname: {
163+
from: '^\\/docs\\/(?:1\\.0|next)',
164+
to: '/abc',
165+
},
166+
},
167+
});
168+
});
169+
});
170+
124171
it('searchParameters.facetFilters search config', () => {
125172
const algolia = {
126173
appId: 'BH4D9OD16A',

packages/docusaurus-theme-search-algolia/src/client/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,6 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8+
export {useAlgoliaThemeConfig} from './useAlgoliaThemeConfig';
89
export {useAlgoliaContextualFacetFilters} from './useAlgoliaContextualFacetFilters';
10+
export {useSearchResultUrlProcessor} from './useSearchResultUrlProcessor';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
8+
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
9+
10+
export function useAlgoliaThemeConfig(): ThemeConfig {
11+
const {
12+
siteConfig: {themeConfig},
13+
} = useDocusaurusContext();
14+
return themeConfig as ThemeConfig;
15+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Copyright (c) Facebook, Inc. and its affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import {useCallback} from 'react';
9+
import {isRegexpStringMatch} from '@docusaurus/theme-common';
10+
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
11+
import {useAlgoliaThemeConfig} from './useAlgoliaThemeConfig';
12+
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
13+
14+
function replacePathname(
15+
pathname: string,
16+
replaceSearchResultPathname: ThemeConfig['algolia']['replaceSearchResultPathname'],
17+
): string {
18+
return replaceSearchResultPathname
19+
? pathname.replaceAll(
20+
new RegExp(replaceSearchResultPathname.from, 'g'),
21+
replaceSearchResultPathname.to,
22+
)
23+
: pathname;
24+
}
25+
26+
/**
27+
* Process the search result url from Algolia to its final form, ready to be
28+
* navigated to or used as a link
29+
*/
30+
export function useSearchResultUrlProcessor(): (url: string) => string {
31+
const {withBaseUrl} = useBaseUrlUtils();
32+
const {
33+
algolia: {externalUrlRegex, replaceSearchResultPathname},
34+
} = useAlgoliaThemeConfig();
35+
36+
return useCallback(
37+
(url: string) => {
38+
const parsedURL = new URL(url);
39+
40+
// Algolia contains an external domain => navigate to URL
41+
if (isRegexpStringMatch(externalUrlRegex, parsedURL.href)) {
42+
return url;
43+
}
44+
45+
// Otherwise => transform to relative URL for SPA navigation
46+
const relativeUrl = `${parsedURL.pathname + parsedURL.hash}`;
47+
48+
return withBaseUrl(
49+
replacePathname(relativeUrl, replaceSearchResultPathname),
50+
);
51+
},
52+
[withBaseUrl, externalUrlRegex, replaceSearchResultPathname],
53+
);
54+
}

packages/docusaurus-theme-search-algolia/src/theme-search-algolia.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,23 @@ declare module '@docusaurus/theme-search-algolia' {
1717
indexName: string;
1818
searchParameters: {[key: string]: unknown};
1919
searchPagePath: string | false | null;
20+
replaceSearchResultPathname?: {
21+
from: string;
22+
to: string;
23+
};
2024
};
2125
};
2226
export type UserThemeConfig = DeepPartial<ThemeConfig>;
2327
}
2428

2529
declare module '@docusaurus/theme-search-algolia/client' {
30+
import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
31+
32+
export function useAlgoliaThemeConfig(): ThemeConfig;
33+
2634
export function useAlgoliaContextualFacetFilters(): [string, string[]];
35+
36+
export function useSearchResultUrlProcessor(): (url: string) => string;
2737
}
2838

2939
declare module '@theme/SearchPage' {

packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.tsx

Lines changed: 16 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@
55
* LICENSE file in the root directory of this source tree.
66
*/
77

8-
import React, {useState, useRef, useCallback, useMemo} from 'react';
9-
import {createPortal} from 'react-dom';
10-
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
11-
import {useHistory} from '@docusaurus/router';
12-
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
13-
import Link from '@docusaurus/Link';
8+
import React, {useCallback, useMemo, useRef, useState} from 'react';
9+
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
1410
import Head from '@docusaurus/Head';
11+
import Link from '@docusaurus/Link';
12+
import {useHistory} from '@docusaurus/router';
1513
import {isRegexpStringMatch} from '@docusaurus/theme-common';
1614
import {useSearchPage} from '@docusaurus/theme-common/internal';
17-
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
18-
import {useAlgoliaContextualFacetFilters} from '@docusaurus/theme-search-algolia/client';
15+
import {
16+
useAlgoliaContextualFacetFilters,
17+
useSearchResultUrlProcessor,
18+
} from '@docusaurus/theme-search-algolia/client';
1919
import Translate from '@docusaurus/Translate';
20+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
21+
import {createPortal} from 'react-dom';
2022
import translations from '@theme/SearchTranslations';
2123

24+
import type {AutocompleteState} from '@algolia/autocomplete-core';
2225
import type {
2326
DocSearchModal as DocSearchModalType,
2427
DocSearchModalProps,
@@ -28,7 +31,6 @@ import type {
2831
StoredDocSearchHit,
2932
} from '@docsearch/react/dist/esm/types';
3033
import type {SearchClient} from 'algoliasearch/lite';
31-
import type {AutocompleteState} from '@algolia/autocomplete-core';
3234

3335
type DocSearchProps = Omit<
3436
DocSearchModalProps,
@@ -88,6 +90,7 @@ function DocSearch({
8890
...props
8991
}: DocSearchProps) {
9092
const {siteMetadata} = useDocusaurusContext();
93+
const processSearchResultUrl = useSearchResultUrlProcessor();
9194

9295
const contextualSearchFacetFilters =
9396
useAlgoliaContextualFacetFilters() as FacetFilters;
@@ -107,7 +110,6 @@ function DocSearch({
107110
facetFilters,
108111
};
109112

110-
const {withBaseUrl} = useBaseUrlUtils();
111113
const history = useHistory();
112114
const searchContainer = useRef<HTMLDivElement | null>(null);
113115
const searchButtonRef = useRef<HTMLButtonElement>(null);
@@ -172,20 +174,10 @@ function DocSearch({
172174

173175
const transformItems = useRef<DocSearchModalProps['transformItems']>(
174176
(items) =>
175-
items.map((item) => {
176-
// If Algolia contains a external domain, we should navigate without
177-
// relative URL
178-
if (isRegexpStringMatch(externalUrlRegex, item.url)) {
179-
return item;
180-
}
181-
182-
// We transform the absolute URL into a relative URL.
183-
const url = new URL(item.url);
184-
return {
185-
...item,
186-
url: withBaseUrl(`${url.pathname}${url.hash}`),
187-
};
188-
}),
177+
items.map((item) => ({
178+
...item,
179+
url: processSearchResultUrl(item.url),
180+
})),
189181
).current;
190182

191183
const resultsFooterComponent: DocSearchProps['resultsFooterComponent'] =

0 commit comments

Comments
 (0)