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(theme-algolia): add option.replaceSearchResultPathname to process/replaceAll search result urls #8428

Merged
merged 11 commits into from Dec 22, 2022
2 changes: 2 additions & 0 deletions packages/docusaurus-theme-common/src/index.ts
Expand Up @@ -28,6 +28,8 @@ export {createStorageSlot, listStorageKeys} from './utils/storageUtils';

export {useContextualSearchFilters} from './utils/searchUtils';

export {getRegexpOrString} from './utils/configUtils';

export {
useCurrentSidebarCategory,
filterDocCardListItems,
Expand Down
21 changes: 21 additions & 0 deletions packages/docusaurus-theme-common/src/utils/configUtils.ts
@@ -0,0 +1,21 @@
/**
* 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.
*/

/**
* Tries to create a RegExp from a string, coming from the Docusaurus config.
* If it fails to create a RegExp it returns the input string.
*
* @param possibleRegexp string that is possibly a regex
* @returns a Regex if possible, otherwise the string
*/
export function getRegexpOrString(possibleRegexp: string): RegExp | string {
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 really like this implementation and would prefer to just support regexp. Adding unit tests would likely reveal the problematic cases.

My idea was more:

  • config accepts both string + RegExp
  • config validation normalizes this as a serializable RegExp string
  • client-side always use new RexExp()

The config normalization can be something like:

if (from instanceof string) {
  return escapeRegexp(from);
} else if (from instanceof RegExp) {
 return from.source;
} else {
  throw new Error('unexpected")
}

=> always return a valid Regexp string source

Does it make sense?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That makes sense; I wasn't terribly happy with getRegexpOrString either. I've tried to address your suggestions in the latest commit, hope I got it right this time 😅

try {
return new RegExp(new RegExp(possibleRegexp).source, 'g');
} catch (e) {
return possibleRegexp;
}
}
Expand Up @@ -121,6 +121,24 @@ describe('validateThemeConfig', () => {
});
});

it('replaceSearchResultPathname config', () => {
const algolia = {
appId: 'BH4D9OD16A',
indexName: 'index',
apiKey: 'apiKey',
replaceSearchResultPathname: {
from: '/docs/',
to: '/',
},
};
expect(testValidateThemeConfig({algolia})).toEqual({
algolia: {
...DEFAULT_CONFIG,
...algolia,
},
});
});

it('searchParameters.facetFilters search config', () => {
const algolia = {
appId: 'BH4D9OD16A',
Expand Down
Expand Up @@ -17,6 +17,10 @@ declare module '@docusaurus/theme-search-algolia' {
indexName: string;
searchParameters: {[key: string]: unknown};
searchPagePath: string | false | null;
replaceSearchResultPathname?: {
from: string;
to: string;
};
};
};
export type UserThemeConfig = DeepPartial<ThemeConfig>;
Expand Down
Expand Up @@ -5,20 +5,21 @@
* LICENSE file in the root directory of this source tree.
*/

import React, {useState, useRef, useCallback, useMemo} from 'react';
import {createPortal} from 'react-dom';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useHistory} from '@docusaurus/router';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import Link from '@docusaurus/Link';
import React, {useCallback, useMemo, useRef, useState} from 'react';
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
import Head from '@docusaurus/Head';
import {isRegexpStringMatch} from '@docusaurus/theme-common';
import Link from '@docusaurus/Link';
import {useHistory} from '@docusaurus/router';
import {isRegexpStringMatch, getRegexpOrString} from '@docusaurus/theme-common';
import {useSearchPage} from '@docusaurus/theme-common/internal';
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
import {useAlgoliaContextualFacetFilters} from '@docusaurus/theme-search-algolia/client';
import Translate from '@docusaurus/Translate';
import {useBaseUrlUtils} from '@docusaurus/useBaseUrl';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {createPortal} from 'react-dom';
import translations from '@theme/SearchTranslations';

import type {AutocompleteState} from '@algolia/autocomplete-core';
import type {
DocSearchModal as DocSearchModalType,
DocSearchModalProps,
Expand All @@ -28,7 +29,6 @@ import type {
StoredDocSearchHit,
} from '@docsearch/react/dist/esm/types';
import type {SearchClient} from 'algoliasearch/lite';
import type {AutocompleteState} from '@algolia/autocomplete-core';

type DocSearchProps = Omit<
DocSearchModalProps,
Expand All @@ -37,6 +37,10 @@ type DocSearchProps = Omit<
contextualSearch?: string;
externalUrlRegex?: string;
searchPagePath: boolean | string;
replaceSearchResultPathname?: {
from: string;
to: string;
};
};

let DocSearchModal: typeof DocSearchModalType | null = null;
Expand Down Expand Up @@ -85,6 +89,7 @@ function mergeFacetFilters(f1: FacetFilters, f2: FacetFilters): FacetFilters {
function DocSearch({
contextualSearch,
externalUrlRegex,
replaceSearchResultPathname,
...props
}: DocSearchProps) {
const {siteMetadata} = useDocusaurusContext();
Expand Down Expand Up @@ -173,14 +178,22 @@ function DocSearch({
const transformItems = useRef<DocSearchModalProps['transformItems']>(
(items) =>
items.map((item) => {
// Replace parts of the URL if the user has added it in the config
const itemUrl = replaceSearchResultPathname
? item.url.replace(
getRegexpOrString(replaceSearchResultPathname.from),
replaceSearchResultPathname.to,
)
: item.url;

// If Algolia contains a external domain, we should navigate without
// relative URL
if (isRegexpStringMatch(externalUrlRegex, item.url)) {
if (isRegexpStringMatch(externalUrlRegex, itemUrl)) {
return item;
}

// We transform the absolute URL into a relative URL.
const url = new URL(item.url);
const url = new URL(itemUrl);
return {
...item,
url: withBaseUrl(`${url.pathname}${url.hash}`),
Expand Down
Expand Up @@ -7,28 +7,29 @@

/* eslint-disable jsx-a11y/no-autofocus */

import React, {useEffect, useState, useReducer, useRef} from 'react';
import React, {useEffect, useReducer, useRef, useState} from 'react';
import clsx from 'clsx';

import algoliaSearch from 'algoliasearch/lite';
import algoliaSearchHelper from 'algoliasearch-helper';
import algoliaSearch from 'algoliasearch/lite';

import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import Head from '@docusaurus/Head';
import Link from '@docusaurus/Link';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
import {
getRegexpOrString,
HtmlClassNameProvider,
usePluralForm,
isRegexpStringMatch,
useEvent,
usePluralForm,
} from '@docusaurus/theme-common';
import {
useTitleFormatter,
useSearchPage,
useTitleFormatter,
} from '@docusaurus/theme-common/internal';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useAllDocsData} from '@docusaurus/plugin-content-docs/client';
import Translate, {translate} from '@docusaurus/Translate';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import Layout from '@theme/Layout';

import type {ThemeConfig} from '@docusaurus/theme-search-algolia';
Expand Down Expand Up @@ -161,7 +162,13 @@ function SearchPageContent(): JSX.Element {
i18n: {currentLocale},
} = useDocusaurusContext();
const {
algolia: {appId, apiKey, indexName, externalUrlRegex},
algolia: {
appId,
apiKey,
indexName,
externalUrlRegex,
replaceSearchResultPathname,
},
} = themeConfig as ThemeConfig;
const documentsFoundPlural = useDocumentsFoundPlural();

Expand Down Expand Up @@ -245,7 +252,14 @@ function SearchPageContent(): JSX.Element {
_highlightResult: {hierarchy: {[key: string]: {value: string}}};
_snippetResult: {content?: {value: string}};
}) => {
const parsedURL = new URL(url);
const parsedURL = new URL(
replaceSearchResultPathname
? url.replace(
getRegexpOrString(replaceSearchResultPathname.from),
replaceSearchResultPathname.to,
)
: url,
);
const titles = Object.keys(hierarchy).map((key) =>
sanitizeValue(hierarchy[key]!.value),
);
Expand Down
Expand Up @@ -39,6 +39,10 @@ export const Schema = Joi.object<ThemeConfig>({
.try(Joi.boolean().invalid(true), Joi.string())
.allow(null)
.default(DEFAULT_CONFIG.searchPagePath),
replaceSearchResultPathname: Joi.object({
from: Joi.string().required(),
to: Joi.string().required(),
}).optional(),
})
.label('themeConfig.algolia')
.required()
Expand Down
6 changes: 6 additions & 0 deletions website/docs/search.md
Expand Up @@ -104,6 +104,12 @@ module.exports = {
// 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: Replace parts of the item URLs from Algolia. Useful when using the same search index for multiple deployments using a different baseUrl. You can use regexp or string in the `from` param. For example: localhost:3000 vs myCompany.com/docs
replaceSearchResultPathname: {
from: '/docs/',
to: '/',
},

// Optional: Algolia search parameters
searchParameters: {},

Expand Down
4 changes: 4 additions & 0 deletions website/docusaurus.config.js
Expand Up @@ -409,6 +409,10 @@ const config = {
appId: 'X1Z85QJPUV',
apiKey: 'bf7211c161e8205da2f933a02534105a',
indexName: 'docusaurus-2',
replaceSearchResultPathname: (isDev || isDeployPreview) ? {
from: '\\/docs\\/next',
to:'/docs'
} : undefined,
},
navbar: {
hideOnScroll: true,
Expand Down
6 changes: 6 additions & 0 deletions website/versioned_docs/version-2.0.1/search.md
Expand Up @@ -104,6 +104,12 @@ module.exports = {
// 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: Replace parts of the item URLs from Algolia. Useful when using the same search index for multiple deployments using a different baseUrl. You can use regexp or string in the `from` param. For example: localhost:3000 vs myCompany.com/docs
replaceSearchResultPathname: {
from: '/docs/',
mellson marked this conversation as resolved.
Show resolved Hide resolved
to: '/',
},

// Optional: Algolia search parameters
searchParameters: {},

Expand Down
6 changes: 6 additions & 0 deletions website/versioned_docs/version-2.1.0/search.md
Expand Up @@ -104,6 +104,12 @@ module.exports = {
// 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: Replace parts of the item URLs from Algolia. Useful when using the same search index for multiple deployments using a different baseUrl. You can use regexp or string in the `from` param. For example: localhost:3000 vs myCompany.com/docs
replaceSearchResultPathname: {
from: '/docs/',
to: '/',
},

// Optional: Algolia search parameters
searchParameters: {},

Expand Down
6 changes: 6 additions & 0 deletions website/versioned_docs/version-2.2.0/search.md
Expand Up @@ -104,6 +104,12 @@ module.exports = {
// 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: Replace parts of the item URLs from Algolia. Useful when using the same search index for multiple deployments using a different baseUrl. You can use regexp or string in the `from` param. For example: localhost:3000 vs myCompany.com/docs
replaceSearchResultPathname: {
from: '/docs/',
to: '/',
},

// Optional: Algolia search parameters
searchParameters: {},

Expand Down