Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from Oct 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
Expand Up @@ -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';
Expand Down Expand Up @@ -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),
Expand Down
Expand Up @@ -11,6 +11,7 @@ import {
isSamePath,
useCollapsible,
Collapsible,
isRegexpStringMatch,
useLocalPathname,
} from '@docusaurus/theme-common';
import type {
Expand All @@ -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)) {
Expand Down
2 changes: 2 additions & 0 deletions packages/docusaurus-theme-common/src/index.ts
Expand Up @@ -93,3 +93,5 @@ export {
useIsomorphicLayoutEffect,
useDynamicCallback,
} from './utils/reactUtils';

export {isRegexpStringMatch} from './utils/regexpUtils';
23 changes: 23 additions & 0 deletions 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(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest moving this to jsUtils. We shouldn't keep on adding more files, especially for these general-purpose utilities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

imho, it's easier to keep it separated on files for an easier search on the editor. (At least for me)
But as @slorber considers :)

Copy link
Collaborator

@Josh-Cena Josh-Cena Oct 28, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That really depends on the editor:D Most decent editors allow you to search globally, but navigating in one file is always easier than in a giant directory. If there are more regex tools to come that we can envision, keeping it seperated can be a good idea, but... I'm not sure if there are many painpoints with regex that need utility functions

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't care too much about the filename as long as the import name is understandable: it's an implementation detail that we can easily refactor later.

Not a fan of a fn with signature RegExp | {test: () => boolean}, what about returning the boolean and calling test() inside directly?

And "" should not be handled like undefined

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep that makes snse to me too, refactored to be much cleaner.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have used a simpler isRegexpStringMatch(regex,str) rather than a higher order function, but it's not too important

regexAsString?: string,
valueToTest?: string,
): boolean {
if (
typeof regexAsString === 'undefined' ||
typeof valueToTest === 'undefined'
) {
return false;
}

return new RegExp(regexAsString, 'gi').test(valueToTest);
}
Expand Up @@ -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',
Expand Down
Expand Up @@ -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';
Expand All @@ -34,7 +35,7 @@ function ResultsFooter({state, onClose}) {
);
}

function DocSearch({contextualSearch, ...props}) {
function DocSearch({contextualSearch, externalUrlRegex, ...props}) {
const {siteMetadata} = useDocusaurusContext();

const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
Expand Down Expand Up @@ -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;
Expand Down
Expand Up @@ -19,6 +19,7 @@ import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {
useTitleFormatter,
usePluralForm,
isRegexpStringMatch,
useDynamicCallback,
} from '@docusaurus/theme-common';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
Expand Down Expand Up @@ -121,7 +122,7 @@ function SearchPage() {
const {
siteConfig: {
themeConfig: {
algolia: {appId, apiKey, indexName},
algolia: {appId, apiKey, indexName, externalUrlRegex},
},
},
i18n: {currentLocale},
Expand Down Expand Up @@ -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)}...`
: '',
Expand Down
Expand Up @@ -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(),
Expand Down
3 changes: 3 additions & 0 deletions website/docs/search.md
Expand Up @@ -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',

Expand Down