feat(search-algolia): algolia externalUrl regex to navigate with window.href (#5795)

This commit is contained in:
Sergio Moreno 2021-10-29 19:53:47 +02:00 committed by GitHub
parent 8c12983a2a
commit adbc02ea38
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 69 additions and 17 deletions

View file

@ -16,6 +16,7 @@ import type {
} from '@theme/NavbarItem/DefaultNavbarItem'; } from '@theme/NavbarItem/DefaultNavbarItem';
import IconExternalLink from '@theme/IconExternalLink'; import IconExternalLink from '@theme/IconExternalLink';
import isInternalUrl from '@docusaurus/isInternalUrl'; import isInternalUrl from '@docusaurus/isInternalUrl';
import {isRegexpStringMatch} from '@docusaurus/theme-common';
import {getInfimaActiveClassName} from './index'; import {getInfimaActiveClassName} from './index';
const dropdownLinkActiveClass = 'dropdown__link--active'; const dropdownLinkActiveClass = 'dropdown__link--active';
@ -54,7 +55,7 @@ export function NavLink({
? { ? {
isActive: (_match, location) => isActive: (_match, location) =>
activeBaseRegex activeBaseRegex
? new RegExp(activeBaseRegex).test(location.pathname) ? isRegexpStringMatch(activeBaseRegex, location.pathname)
: location.pathname.startsWith(activeBaseUrl), : location.pathname.startsWith(activeBaseUrl),
} }
: null), : null),

View file

@ -11,6 +11,7 @@ import {
isSamePath, isSamePath,
useCollapsible, useCollapsible,
Collapsible, Collapsible,
isRegexpStringMatch,
useLocalPathname, useLocalPathname,
} from '@docusaurus/theme-common'; } from '@docusaurus/theme-common';
import type { import type {
@ -31,10 +32,7 @@ function isItemActive(
if (isSamePath(item.to, localPathname)) { if (isSamePath(item.to, localPathname)) {
return true; return true;
} }
if ( if (isRegexpStringMatch(item.activeBaseRegex, localPathname)) {
item.activeBaseRegex &&
new RegExp(item.activeBaseRegex).test(localPathname)
) {
return true; return true;
} }
if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) { if (item.activeBasePath && localPathname.startsWith(item.activeBasePath)) {

View file

@ -93,3 +93,5 @@ export {
useIsomorphicLayoutEffect, useIsomorphicLayoutEffect,
useDynamicCallback, useDynamicCallback,
} from './utils/reactUtils'; } from './utils/reactUtils';
export {isRegexpStringMatch} from './utils/regexpUtils';

View file

@ -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);
}

View file

@ -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', () => { test('searchParameters.facetFilters search config', () => {
const algolia = { const algolia = {
indexName: 'index', indexName: 'index',

View file

@ -13,6 +13,7 @@ import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link'; import Link from '@docusaurus/Link';
import Head from '@docusaurus/Head'; import Head from '@docusaurus/Head';
import useSearchQuery from '@theme/hooks/useSearchQuery'; import useSearchQuery from '@theme/hooks/useSearchQuery';
import {isRegexpStringMatch} from '@docusaurus/theme-common';
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react'; import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
import useAlgoliaContextualFacetFilters from '@theme/hooks/useAlgoliaContextualFacetFilters'; import useAlgoliaContextualFacetFilters from '@theme/hooks/useAlgoliaContextualFacetFilters';
import {translate} from '@docusaurus/Translate'; 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 {siteMetadata} = useDocusaurusContext();
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters(); const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
@ -102,21 +103,28 @@ function DocSearch({contextualSearch, ...props}) {
const navigator = useRef({ const navigator = useRef({
navigate({itemUrl}) { 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; }).current;
const transformItems = useRef((items) => { const transformItems = useRef((items) => {
return items.map((item) => { return items.map((item) => {
// We transform the absolute URL into a relative URL. // If Algolia contains a external domain, we should navigate without relative URL
// Alternatively, we can use `new URL(item.url)` but it's not if (isRegexpStringMatch(externalUrlRegex, item.url)) {
// supported in IE. return item;
const a = document.createElement('a'); }
a.href = item.url;
// We transform the absolute URL into a relative URL.
const url = new URL(item.url);
return { return {
...item, ...item,
url: withBaseUrl(`${a.pathname}${a.hash}`), url: withBaseUrl(`${url.pathname}${url.hash}`),
}; };
}); });
}).current; }).current;

View file

@ -19,6 +19,7 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import { import {
useTitleFormatter, useTitleFormatter,
usePluralForm, usePluralForm,
isRegexpStringMatch,
useDynamicCallback, useDynamicCallback,
} from '@docusaurus/theme-common'; } from '@docusaurus/theme-common';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
@ -121,7 +122,7 @@ function SearchPage() {
const { const {
siteConfig: { siteConfig: {
themeConfig: { themeConfig: {
algolia: {appId, apiKey, indexName}, algolia: {appId, apiKey, indexName, externalUrlRegex},
}, },
}, },
i18n: {currentLocale}, i18n: {currentLocale},
@ -205,14 +206,16 @@ function SearchPage() {
_highlightResult: {hierarchy}, _highlightResult: {hierarchy},
_snippetResult: snippet = {}, _snippetResult: snippet = {},
}) => { }) => {
const {pathname, hash} = new URL(url); const parsedURL = new URL(url);
const titles = Object.keys(hierarchy).map((key) => { const titles = Object.keys(hierarchy).map((key) => {
return sanitizeValue(hierarchy[key].value); return sanitizeValue(hierarchy[key].value);
}); });
return { return {
title: titles.pop(), title: titles.pop(),
url: pathname + hash, url: isRegexpStringMatch(externalUrlRegex, parsedURL.href)
? parsedURL.href
: parsedURL.pathname + parsedURL.hash,
summary: snippet.content summary: snippet.content
? `${sanitizeValue(snippet.content.value)}...` ? `${sanitizeValue(snippet.content.value)}...`
: '', : '',

View file

@ -22,7 +22,7 @@ const Schema = Joi.object({
algolia: Joi.object({ algolia: Joi.object({
// Docusaurus attributes // Docusaurus attributes
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch), contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
externalUrlRegex: Joi.string().optional(),
// Algolia attributes // Algolia attributes
appId: Joi.string().default(DEFAULT_CONFIG.appId), appId: Joi.string().default(DEFAULT_CONFIG.appId),
apiKey: Joi.string().required(), apiKey: Joi.string().required(),

View file

@ -86,6 +86,9 @@ module.exports = {
// Optional: see doc section below // Optional: see doc section below
contextualSearch: true, 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 // Optional: see doc section below
appId: 'YOUR_APP_ID', appId: 'YOUR_APP_ID',