diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem.tsx index 5ae631be0181..5ae65695c3cc 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DefaultNavbarItem.tsx @@ -16,6 +16,7 @@ import type { } from '@theme/NavbarItem/DefaultNavbarItem'; import IconExternalLink from '@theme/IconExternalLink'; import isInternalUrl from '@docusaurus/isInternalUrl'; +import {isRegexpStringMatch} from '@docusaurus/theme-common'; import {getInfimaActiveClassName} from './index'; const dropdownLinkActiveClass = 'dropdown__link--active'; @@ -54,7 +55,7 @@ export function NavLink({ ? { isActive: (_match, location) => activeBaseRegex - ? new RegExp(activeBaseRegex).test(location.pathname) + ? isRegexpStringMatch(activeBaseRegex, location.pathname) : location.pathname.startsWith(activeBaseUrl), } : null), diff --git a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem.tsx b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem.tsx index 35459e2b556a..11a7cc795a39 100644 --- a/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem.tsx +++ b/packages/docusaurus-theme-classic/src/theme/NavbarItem/DropdownNavbarItem.tsx @@ -11,6 +11,7 @@ import { isSamePath, useCollapsible, Collapsible, + isRegexpStringMatch, useLocalPathname, } from '@docusaurus/theme-common'; import type { @@ -31,10 +32,7 @@ function isItemActive( if (isSamePath(item.to, localPathname)) { return true; } - if ( - item.activeBaseRegex && - new RegExp(item.activeBaseRegex).test(localPathname) - ) { + if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) { return true; } if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) { diff --git a/packages/docusaurus-theme-common/src/index.ts b/packages/docusaurus-theme-common/src/index.ts index 48874061623f..c8e66543a17f 100644 --- a/packages/docusaurus-theme-common/src/index.ts +++ b/packages/docusaurus-theme-common/src/index.ts @@ -93,3 +93,5 @@ export { useIsomorphicLayoutEffect, useDynamicCallback, } from './utils/reactUtils'; + +export {isRegexpStringMatch} from './utils/regexpUtils'; diff --git a/packages/docusaurus-theme-common/src/utils/regexpUtils.ts b/packages/docusaurus-theme-common/src/utils/regexpUtils.ts new file mode 100644 index 000000000000..a4f3cdd63cb8 --- /dev/null +++ b/packages/docusaurus-theme-common/src/utils/regexpUtils.ts @@ -0,0 +1,23 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * Utility to convert an optional string into a Regex case sensitive and global + */ +export function isRegexpStringMatch( + regexAsString?: string, + valueToTest?: string, +): boolean { + if ( + typeof regexAsString === 'undefined' || + typeof valueToTest === 'undefined' + ) { + return false; + } + + return new RegExp(regexAsString, 'gi').test(valueToTest); +} diff --git a/packages/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.js b/packages/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.js index f6df87f5a1fe..6725671573c6 100644 --- a/packages/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.js +++ b/packages/docusaurus-theme-search-algolia/src/__tests__/validateThemeConfig.test.js @@ -103,6 +103,20 @@ describe('validateThemeConfig', () => { }); }); + test('externalUrlRegex config', () => { + const algolia = { + indexName: 'index', + apiKey: 'apiKey', + externalUrlRegex: 'http://external-domain.com', + }; + expect(testValidateThemeConfig({algolia})).toEqual({ + algolia: { + ...DEFAULT_CONFIG, + ...algolia, + }, + }); + }); + test('searchParameters.facetFilters search config', () => { const algolia = { indexName: 'index', diff --git a/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.js b/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.js index bde8d2d68dfb..b61c86f418fa 100644 --- a/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.js +++ b/packages/docusaurus-theme-search-algolia/src/theme/SearchBar/index.js @@ -13,6 +13,7 @@ import {useBaseUrlUtils} from '@docusaurus/useBaseUrl'; import Link from '@docusaurus/Link'; import Head from '@docusaurus/Head'; import useSearchQuery from '@theme/hooks/useSearchQuery'; +import {isRegexpStringMatch} from '@docusaurus/theme-common'; import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react'; import useAlgoliaContextualFacetFilters from '@theme/hooks/useAlgoliaContextualFacetFilters'; import {translate} from '@docusaurus/Translate'; @@ -34,7 +35,7 @@ function ResultsFooter({state, onClose}) { ); } -function DocSearch({contextualSearch, ...props}) { +function DocSearch({contextualSearch, externalUrlRegex, ...props}) { const {siteMetadata} = useDocusaurusContext(); const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters(); @@ -102,21 +103,28 @@ function DocSearch({contextualSearch, ...props}) { const navigator = useRef({ navigate({itemUrl}) { - history.push(itemUrl); + // Algolia results could contain URL's from other domains which cannot + // be served through history and should navigate with window.location + if (isRegexpStringMatch(externalUrlRegex, itemUrl)) { + window.location.href = itemUrl; + } else { + history.push(itemUrl); + } }, }).current; const transformItems = useRef((items) => { return items.map((item) => { - // We transform the absolute URL into a relative URL. - // Alternatively, we can use `new URL(item.url)` but it's not - // supported in IE. - const a = document.createElement('a'); - a.href = item.url; + // If Algolia contains a external domain, we should navigate without relative URL + if (isRegexpStringMatch(externalUrlRegex, item.url)) { + return item; + } + // We transform the absolute URL into a relative URL. + const url = new URL(item.url); return { ...item, - url: withBaseUrl(`${a.pathname}${a.hash}`), + url: withBaseUrl(`${url.pathname}${url.hash}`), }; }); }).current; diff --git a/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.js b/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.js index 8c3ac8b7d7a6..b89e233f7891 100644 --- a/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.js +++ b/packages/docusaurus-theme-search-algolia/src/theme/SearchPage/index.js @@ -19,6 +19,7 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; import { useTitleFormatter, usePluralForm, + isRegexpStringMatch, useDynamicCallback, } from '@docusaurus/theme-common'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; @@ -121,7 +122,7 @@ function SearchPage() { const { siteConfig: { themeConfig: { - algolia: {appId, apiKey, indexName}, + algolia: {appId, apiKey, indexName, externalUrlRegex}, }, }, i18n: {currentLocale}, @@ -205,14 +206,16 @@ function SearchPage() { _highlightResult: {hierarchy}, _snippetResult: snippet = {}, }) => { - const {pathname, hash} = new URL(url); + const parsedURL = new URL(url); const titles = Object.keys(hierarchy).map((key) => { return sanitizeValue(hierarchy[key].value); }); return { title: titles.pop(), - url: pathname + hash, + url: isRegexpStringMatch(externalUrlRegex, parsedURL.href) + ? parsedURL.href + : parsedURL.pathname + parsedURL.hash, summary: snippet.content ? `${sanitizeValue(snippet.content.value)}...` : '', diff --git a/packages/docusaurus-theme-search-algolia/src/validateThemeConfig.js b/packages/docusaurus-theme-search-algolia/src/validateThemeConfig.js index 1c514229b732..8cfe4bfed9e3 100644 --- a/packages/docusaurus-theme-search-algolia/src/validateThemeConfig.js +++ b/packages/docusaurus-theme-search-algolia/src/validateThemeConfig.js @@ -22,7 +22,7 @@ const Schema = Joi.object({ algolia: Joi.object({ // Docusaurus attributes contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch), - + externalUrlRegex: Joi.string().optional(), // Algolia attributes appId: Joi.string().default(DEFAULT_CONFIG.appId), apiKey: Joi.string().required(), diff --git a/website/docs/search.md b/website/docs/search.md index 26fd0e483bc3..ce9f98699661 100644 --- a/website/docs/search.md +++ b/website/docs/search.md @@ -86,6 +86,9 @@ module.exports = { // Optional: see doc section below contextualSearch: true, + // Optional: Specify domains where the navigation should occur through window.location instead on history.push. Useful when our Algolia config crawls multiple documentation sites and we want to navigate with window.location.href to them. + externalUrlRegex: 'external\\.com|domain\\.com', + // Optional: see doc section below appId: 'YOUR_APP_ID',