mirror of
https://github.com/facebook/docusaurus.git
synced 2025-05-12 16:47:26 +02:00
feat(v2): contextual search, dynamic Algolia facetFilters (#3550)
* POC of contextual search dynamic filters * fix useSearchTags bugs * contextual search should use preferred version (persisted in storage) * Contextual search: make system decoupled from algolia + wire proper meta tags and facet filters * rework doc tag + minor refactorings * update snapshots * polish contextual search * add Algolia validateThemeConfig tests
This commit is contained in:
parent
d23940c9c3
commit
21264f5ed0
21 changed files with 468 additions and 138 deletions
|
@ -302,6 +302,7 @@ Object {
|
|||
\\"version\\": \\"current\\"
|
||||
}",
|
||||
"version-current-metadata-prop-751.json": "{
|
||||
\\"pluginId\\": \\"default\\",
|
||||
\\"version\\": \\"current\\",
|
||||
\\"label\\": \\"Next\\",
|
||||
\\"isLast\\": true,
|
||||
|
@ -640,6 +641,7 @@ Object {
|
|||
\\"sidebar\\": \\"version-1.0.0/community\\"
|
||||
}",
|
||||
"version-1-0-0-metadata-prop-608.json": "{
|
||||
\\"pluginId\\": \\"community\\",
|
||||
\\"version\\": \\"1.0.0\\",
|
||||
\\"label\\": \\"1.0.0\\",
|
||||
\\"isLast\\": true,
|
||||
|
@ -657,6 +659,7 @@ Object {
|
|||
}
|
||||
}",
|
||||
"version-current-metadata-prop-751.json": "{
|
||||
\\"pluginId\\": \\"community\\",
|
||||
\\"version\\": \\"current\\",
|
||||
\\"label\\": \\"Next\\",
|
||||
\\"isLast\\": false,
|
||||
|
@ -1102,6 +1105,7 @@ Object {
|
|||
\\"version\\": \\"withSlugs\\"
|
||||
}",
|
||||
"version-1-0-0-metadata-prop-608.json": "{
|
||||
\\"pluginId\\": \\"default\\",
|
||||
\\"version\\": \\"1.0.0\\",
|
||||
\\"label\\": \\"1.0.0\\",
|
||||
\\"isLast\\": false,
|
||||
|
@ -1145,6 +1149,7 @@ Object {
|
|||
}
|
||||
}",
|
||||
"version-1-0-1-metadata-prop-e87.json": "{
|
||||
\\"pluginId\\": \\"default\\",
|
||||
\\"version\\": \\"1.0.1\\",
|
||||
\\"label\\": \\"1.0.1\\",
|
||||
\\"isLast\\": true,
|
||||
|
@ -1182,6 +1187,7 @@ Object {
|
|||
}
|
||||
}",
|
||||
"version-current-metadata-prop-751.json": "{
|
||||
\\"pluginId\\": \\"default\\",
|
||||
\\"version\\": \\"current\\",
|
||||
\\"label\\": \\"Next\\",
|
||||
\\"isLast\\": false,
|
||||
|
@ -1219,6 +1225,7 @@ Object {
|
|||
}
|
||||
}",
|
||||
"version-with-slugs-metadata-prop-2bf.json": "{
|
||||
\\"pluginId\\": \\"default\\",
|
||||
\\"version\\": \\"withSlugs\\",
|
||||
\\"label\\": \\"withSlugs\\",
|
||||
\\"isLast\\": false,
|
||||
|
|
|
@ -267,7 +267,11 @@ export default function pluginContentDocs(
|
|||
`${docuHash(
|
||||
`version-${loadedVersion.versionName}-metadata-prop`,
|
||||
)}.json`,
|
||||
JSON.stringify(toVersionMetadataProp(loadedVersion), null, 2),
|
||||
JSON.stringify(
|
||||
toVersionMetadataProp(pluginId, loadedVersion),
|
||||
null,
|
||||
2,
|
||||
),
|
||||
);
|
||||
|
||||
addRoute({
|
||||
|
|
|
@ -13,6 +13,7 @@ declare module '@docusaurus/plugin-content-docs-types' {
|
|||
};
|
||||
|
||||
export type PropVersionMetadata = {
|
||||
pluginId: string;
|
||||
version: string;
|
||||
label: string;
|
||||
isLast: boolean;
|
||||
|
|
|
@ -62,9 +62,11 @@ Available document ids=
|
|||
}
|
||||
|
||||
export function toVersionMetadataProp(
|
||||
pluginId: string,
|
||||
loadedVersion: LoadedVersion,
|
||||
): PropVersionMetadata {
|
||||
return {
|
||||
pluginId,
|
||||
version: loadedVersion.versionName,
|
||||
label: loadedVersion.versionLabel,
|
||||
isLast: loadedVersion.isLast,
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
getActiveDocContext,
|
||||
getDocVersionSuggestions,
|
||||
GetActivePluginOptions,
|
||||
ActivePlugin,
|
||||
} from '../../client/docsClientUtils';
|
||||
|
||||
export const useAllDocsData = (): Record<string, GlobalPluginData> =>
|
||||
|
@ -33,6 +34,23 @@ export const useActivePlugin = (options: GetActivePluginOptions = {}) => {
|
|||
return getActivePlugin(data, pathname, options);
|
||||
};
|
||||
|
||||
export const useActivePluginAndVersion = (
|
||||
options: GetActivePluginOptions = {},
|
||||
):
|
||||
| undefined
|
||||
| {activePlugin: ActivePlugin; activeVersion: GlobalVersion | undefined} => {
|
||||
const activePlugin = useActivePlugin(options);
|
||||
const {pathname} = useLocation();
|
||||
if (activePlugin) {
|
||||
const activeVersion = getActiveVersion(activePlugin.pluginData, pathname);
|
||||
return {
|
||||
activePlugin,
|
||||
activeVersion,
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
// versions are returned ordered (most recent first)
|
||||
export const useVersions = (pluginId: string | undefined): GlobalVersion[] => {
|
||||
const data = useDocsData(pluginId);
|
||||
|
|
|
@ -18,9 +18,9 @@ import NotFound from '@theme/NotFound';
|
|||
import type {DocumentRoute} from '@theme/DocItem';
|
||||
import type {Props} from '@theme/DocPage';
|
||||
import {matchPath} from '@docusaurus/router';
|
||||
import Head from '@docusaurus/Head';
|
||||
|
||||
import styles from './styles.module.css';
|
||||
import {docVersionSearchTag} from '../../utils/searchUtils';
|
||||
|
||||
type DocPageContentProps = {
|
||||
readonly currentDocRoute: DocumentRoute;
|
||||
|
@ -28,43 +28,22 @@ type DocPageContentProps = {
|
|||
readonly children: ReactNode;
|
||||
};
|
||||
|
||||
// This theme is not coupled to Algolia, but can we do something else?
|
||||
// Note the last version is also indexed with "last", to avoid breaking search on new releases
|
||||
// See https://github.com/facebook/docusaurus/issues/3391
|
||||
function DocSearchVersionHeader({
|
||||
version,
|
||||
isLast,
|
||||
}: {
|
||||
version: string;
|
||||
isLast: boolean;
|
||||
}) {
|
||||
const versions = isLast ? [version, 'latest'] : [version];
|
||||
return (
|
||||
<Head>
|
||||
<meta
|
||||
name="docsearch:version"
|
||||
content={
|
||||
// See https://github.com/facebook/docusaurus/issues/3391#issuecomment-685594160
|
||||
versions.join(',')
|
||||
}
|
||||
/>
|
||||
</Head>
|
||||
);
|
||||
}
|
||||
|
||||
function DocPageContent({
|
||||
currentDocRoute,
|
||||
versionMetadata,
|
||||
children,
|
||||
}: DocPageContentProps): JSX.Element {
|
||||
const {siteConfig, isClient} = useDocusaurusContext();
|
||||
const {permalinkToSidebar, docsSidebars, version, isLast} = versionMetadata;
|
||||
const {pluginId, permalinkToSidebar, docsSidebars, version} = versionMetadata;
|
||||
const sidebarName = permalinkToSidebar[currentDocRoute.path];
|
||||
const sidebar = docsSidebars[sidebarName];
|
||||
return (
|
||||
<>
|
||||
<DocSearchVersionHeader version={version} isLast={isLast} />
|
||||
<Layout key={isClient}>
|
||||
<Layout
|
||||
key={isClient}
|
||||
searchMetadatas={{
|
||||
version,
|
||||
tag: docVersionSearchTag(pluginId, version),
|
||||
}}>
|
||||
<div className={styles.docPage}>
|
||||
{sidebar && (
|
||||
<div className={styles.docSidebarContainer} role="complementary">
|
||||
|
@ -87,7 +66,6 @@ function DocPageContent({
|
|||
</main>
|
||||
</div>
|
||||
</Layout>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -7,99 +7,26 @@
|
|||
|
||||
import React from 'react';
|
||||
import clsx from 'clsx';
|
||||
import Head from '@docusaurus/Head';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
|
||||
import ThemeProvider from '@theme/ThemeProvider';
|
||||
import UserPreferencesProvider from '@theme/UserPreferencesProvider';
|
||||
import AnnouncementBar from '@theme/AnnouncementBar';
|
||||
import Navbar from '@theme/Navbar';
|
||||
import Footer from '@theme/Footer';
|
||||
import LayoutProviders from '@theme/LayoutProviders';
|
||||
import LayoutHead from '@theme/LayoutHead';
|
||||
import type {Props} from '@theme/Layout';
|
||||
|
||||
import './styles.css';
|
||||
import DocsPreferredVersionContextProvider from '../../utils/docsPreferredVersion/DocsPreferredVersionProvider';
|
||||
|
||||
function Providers({children}) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<UserPreferencesProvider>
|
||||
<DocsPreferredVersionContextProvider>
|
||||
{children}
|
||||
</DocsPreferredVersionContextProvider>
|
||||
</UserPreferencesProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function Layout(props: Props): JSX.Element {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const {
|
||||
favicon,
|
||||
title: siteTitle,
|
||||
themeConfig: {image: defaultImage, metadatas},
|
||||
url: siteUrl,
|
||||
titleDelimiter,
|
||||
} = siteConfig;
|
||||
const {
|
||||
children,
|
||||
title,
|
||||
noFooter,
|
||||
description,
|
||||
image,
|
||||
keywords,
|
||||
permalink,
|
||||
wrapperClassName,
|
||||
} = props;
|
||||
const metaTitle = title
|
||||
? `${title} ${titleDelimiter} ${siteTitle}`
|
||||
: siteTitle;
|
||||
const metaImage = image || defaultImage;
|
||||
const metaImageUrl = useBaseUrl(metaImage, {absolute: true});
|
||||
const faviconUrl = useBaseUrl(favicon);
|
||||
const {children, noFooter, wrapperClassName} = props;
|
||||
return (
|
||||
<Providers>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" />
|
||||
{metaTitle && <title>{metaTitle}</title>}
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && (
|
||||
<meta property="og:description" content={description} />
|
||||
)}
|
||||
{keywords && keywords.length && (
|
||||
<meta name="keywords" content={keywords.join(',')} />
|
||||
)}
|
||||
{metaImage && <meta property="og:image" content={metaImageUrl} />}
|
||||
{metaImage && <meta property="twitter:image" content={metaImageUrl} />}
|
||||
{metaImage && (
|
||||
<meta name="twitter:image:alt" content={`Image for ${metaTitle}`} />
|
||||
)}
|
||||
{permalink && <meta property="og:url" content={siteUrl + permalink} />}
|
||||
{permalink && <link rel="canonical" href={siteUrl + permalink} />}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</Head>
|
||||
|
||||
<Head
|
||||
// it's important to have an additional <Head> element here,
|
||||
// as it allows react-helmet to override values set in previous <Head>
|
||||
// ie we can override default metadatas such as "twitter:card"
|
||||
// In same Head, the same meta would appear twice instead of overriding
|
||||
// See react-helmet doc
|
||||
>
|
||||
{metadatas.map((metadata, i) => (
|
||||
<meta key={`metadata_${i}`} {...metadata} />
|
||||
))}
|
||||
</Head>
|
||||
<LayoutProviders>
|
||||
<LayoutHead {...props} />
|
||||
|
||||
<AnnouncementBar />
|
||||
<Navbar />
|
||||
<div className={clsx('main-wrapper', wrapperClassName)}>{children}</div>
|
||||
|
||||
{!noFooter && <Footer />}
|
||||
</Providers>
|
||||
</LayoutProviders>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import Head from '@docusaurus/Head';
|
||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
||||
import useBaseUrl from '@docusaurus/useBaseUrl';
|
||||
import type {Props} from '@theme/Layout';
|
||||
import SearchMetadatas from '@theme/SearchMetadatas';
|
||||
import {DEFAULT_SEARCH_TAG} from '../../utils/searchUtils';
|
||||
|
||||
export default function LayoutHead(props: Props): JSX.Element {
|
||||
const {siteConfig} = useDocusaurusContext();
|
||||
const {
|
||||
favicon,
|
||||
title: siteTitle,
|
||||
themeConfig: {image: defaultImage, metadatas},
|
||||
url: siteUrl,
|
||||
titleDelimiter,
|
||||
} = siteConfig;
|
||||
const {
|
||||
title,
|
||||
description,
|
||||
image,
|
||||
keywords,
|
||||
permalink,
|
||||
searchMetadatas,
|
||||
} = props;
|
||||
const metaTitle = title
|
||||
? `${title} ${titleDelimiter} ${siteTitle}`
|
||||
: siteTitle;
|
||||
const metaImage = image || defaultImage;
|
||||
const metaImageUrl = useBaseUrl(metaImage, {absolute: true});
|
||||
const faviconUrl = useBaseUrl(favicon);
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
{/* TODO: Do not assume that it is in english language */}
|
||||
<html lang="en" />
|
||||
{metaTitle && <title>{metaTitle}</title>}
|
||||
{metaTitle && <meta property="og:title" content={metaTitle} />}
|
||||
{favicon && <link rel="shortcut icon" href={faviconUrl} />}
|
||||
{description && <meta name="description" content={description} />}
|
||||
{description && (
|
||||
<meta property="og:description" content={description} />
|
||||
)}
|
||||
{keywords && keywords.length && (
|
||||
<meta name="keywords" content={keywords.join(',')} />
|
||||
)}
|
||||
{metaImage && <meta property="og:image" content={metaImageUrl} />}
|
||||
{metaImage && <meta property="twitter:image" content={metaImageUrl} />}
|
||||
{metaImage && (
|
||||
<meta name="twitter:image:alt" content={`Image for ${metaTitle}`} />
|
||||
)}
|
||||
{permalink && <meta property="og:url" content={siteUrl + permalink} />}
|
||||
{permalink && <link rel="canonical" href={siteUrl + permalink} />}
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
</Head>
|
||||
|
||||
<SearchMetadatas
|
||||
tag={DEFAULT_SEARCH_TAG}
|
||||
language="en" // TODO i18n
|
||||
{...searchMetadatas}
|
||||
/>
|
||||
|
||||
<Head
|
||||
// it's important to have an additional <Head> element here,
|
||||
// as it allows react-helmet to override values set in previous <Head>
|
||||
// ie we can override default metadatas such as "twitter:card"
|
||||
// In same Head, the same meta would appear twice instead of overriding
|
||||
// See react-helmet doc
|
||||
>
|
||||
{metadatas.map((metadata, i) => (
|
||||
<meta key={`metadata_${i}`} {...metadata} />
|
||||
))}
|
||||
</Head>
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import ThemeProvider from '@theme/ThemeProvider';
|
||||
import UserPreferencesProvider from '@theme/UserPreferencesProvider';
|
||||
import DocsPreferredVersionContextProvider from '../../utils/docsPreferredVersion/DocsPreferredVersionProvider';
|
||||
|
||||
export default function LayoutProviders({children}) {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<UserPreferencesProvider>
|
||||
<DocsPreferredVersionContextProvider>
|
||||
{children}
|
||||
</DocsPreferredVersionContextProvider>
|
||||
</UserPreferencesProvider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Head from '@docusaurus/Head';
|
||||
|
||||
type SearchTagMetaProps = {
|
||||
language?: string;
|
||||
version?: string;
|
||||
tag?: string;
|
||||
};
|
||||
|
||||
// Note: we don't couple this to Algolia/DocSearch on purpose
|
||||
// We may want to support other search engine plugins too
|
||||
// Search plugins should swizzle/override this comp to add their behavior
|
||||
export default function SearchMetadatas({
|
||||
language,
|
||||
version,
|
||||
tag,
|
||||
}: SearchTagMetaProps) {
|
||||
return (
|
||||
<Head>
|
||||
{language && <meta name="docusaurus_language" content={`${language}`} />}
|
||||
{version && <meta name="docusaurus_version" content={version} />}
|
||||
{tag && <meta name="docusaurus_tag" content={tag} />}
|
||||
</Head>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
import {useAllDocsData, useActivePluginAndVersion} from '@theme/hooks/useDocs';
|
||||
import {useDocsPreferredVersionByPluginId} from '../../utils/docsPreferredVersion/useDocsPreferredVersion';
|
||||
import {DEFAULT_SEARCH_TAG, docVersionSearchTag} from '../../utils/searchUtils';
|
||||
|
||||
type ContextualSearchFilters = {
|
||||
language: string;
|
||||
tags: string[];
|
||||
};
|
||||
|
||||
// We may want to support multiple search engines, don't couple that to Algolia/DocSearch
|
||||
// Maybe users will want to use its own search engine solution
|
||||
export default function useContextualSearchFilters(): ContextualSearchFilters {
|
||||
const allDocsData = useAllDocsData();
|
||||
const activePluginAndVersion = useActivePluginAndVersion();
|
||||
const docsPreferredVersionByPluginId = useDocsPreferredVersionByPluginId();
|
||||
|
||||
function getDocPluginTags(pluginId: string) {
|
||||
const activeVersion =
|
||||
activePluginAndVersion?.activePlugin?.pluginId === pluginId
|
||||
? activePluginAndVersion.activeVersion
|
||||
: undefined;
|
||||
|
||||
const preferredVersion = docsPreferredVersionByPluginId[pluginId];
|
||||
|
||||
const latestVersion = allDocsData[pluginId].versions.find((v) => v.isLast);
|
||||
|
||||
const version = activeVersion ?? preferredVersion ?? latestVersion;
|
||||
|
||||
return docVersionSearchTag(pluginId, version.name);
|
||||
}
|
||||
|
||||
const language = 'en'; // TODO i18n
|
||||
|
||||
const tags = [
|
||||
DEFAULT_SEARCH_TAG,
|
||||
...Object.keys(allDocsData).map(getDocPluginTags),
|
||||
];
|
||||
|
||||
return {
|
||||
language,
|
||||
tags,
|
||||
};
|
||||
}
|
|
@ -246,6 +246,10 @@ declare module '@theme/Layout' {
|
|||
keywords?: string[];
|
||||
permalink?: string;
|
||||
wrapperClassName?: string;
|
||||
searchMetadatas?: {
|
||||
version?: string;
|
||||
tag?: string;
|
||||
};
|
||||
};
|
||||
|
||||
const Layout: (props: Props) => JSX.Element;
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
*/
|
||||
import {useCallback} from 'react';
|
||||
import {useDocsPreferredVersionContext} from './DocsPreferredVersionProvider';
|
||||
import {useDocsData} from '@theme/hooks/useDocs';
|
||||
import {useAllDocsData, useDocsData} from '@theme/hooks/useDocs';
|
||||
|
||||
import {DEFAULT_PLUGIN_ID} from '@docusaurus/constants';
|
||||
|
||||
|
@ -32,3 +32,31 @@ export default function useDocsPreferredVersion(
|
|||
|
||||
return {preferredVersion, savePreferredVersionName} as const;
|
||||
}
|
||||
|
||||
export function useDocsPreferredVersionByPluginId() {
|
||||
const allDocsData = useAllDocsData();
|
||||
const [state] = useDocsPreferredVersionContext();
|
||||
|
||||
function getPluginIdPreferredVersion(pluginId: string) {
|
||||
const docsData = allDocsData[pluginId];
|
||||
const {preferredVersionName} = state[pluginId];
|
||||
|
||||
return preferredVersionName
|
||||
? docsData.versions.find(
|
||||
(version) => version.name === preferredVersionName,
|
||||
)
|
||||
: null;
|
||||
}
|
||||
|
||||
const pluginIds = Object.keys(allDocsData);
|
||||
|
||||
const result: Record<
|
||||
string,
|
||||
any // TODO find a way to type this properly!
|
||||
> = {};
|
||||
pluginIds.forEach((pluginId) => {
|
||||
result[pluginId] = getPluginIdPreferredVersion(pluginId);
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
12
packages/docusaurus-theme-classic/src/utils/searchUtils.ts
Normal file
12
packages/docusaurus-theme-classic/src/utils/searchUtils.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export const DEFAULT_SEARCH_TAG = 'default';
|
||||
|
||||
export function docVersionSearchTag(pluginId: string, versionName: string) {
|
||||
return `docs-${pluginId}-${versionName}`;
|
||||
}
|
|
@ -88,4 +88,50 @@ describe('validateThemeConfig', () => {
|
|||
testValidateThemeConfig({algolia}),
|
||||
).toThrowErrorMatchingInlineSnapshot(`"\\"algolia.apiKey\\" is required"`);
|
||||
});
|
||||
|
||||
test('contextualSearch config', () => {
|
||||
const algolia = {
|
||||
indexName: 'index',
|
||||
apiKey: 'apiKey',
|
||||
contextualSearch: true,
|
||||
};
|
||||
expect(testValidateThemeConfig({algolia})).toEqual({
|
||||
algolia: {
|
||||
...DEFAULT_CONFIG,
|
||||
...algolia,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('searchParameters.facetFilters search config', () => {
|
||||
const algolia = {
|
||||
indexName: 'index',
|
||||
apiKey: 'apiKey',
|
||||
searchParameters: {
|
||||
facetFilters: ['version:1.0'],
|
||||
},
|
||||
};
|
||||
expect(testValidateThemeConfig({algolia})).toEqual({
|
||||
algolia: {
|
||||
...DEFAULT_CONFIG,
|
||||
...algolia,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('contextualSearch + searchParameters.facetFilters config', () => {
|
||||
const algolia = {
|
||||
indexName: 'index',
|
||||
apiKey: 'apiKey',
|
||||
contextualSearch: true,
|
||||
searchParameters: {
|
||||
facetFilters: ['version:1.0'],
|
||||
},
|
||||
};
|
||||
expect(() =>
|
||||
testValidateThemeConfig({algolia}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"If you are using algolia.contextualSearch: true, you should not provide algolia.searchParameters.facetFilters, as it is computed for you dynamically"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -14,6 +14,7 @@ import Link from '@docusaurus/Link';
|
|||
import Head from '@docusaurus/Head';
|
||||
import useSearchQuery from '@theme/hooks/useSearchQuery';
|
||||
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
|
||||
import {useAlgoliaContextualSearchParameters} from '../../utils/algoliaSearchUtils';
|
||||
|
||||
let DocSearchModal = null;
|
||||
|
||||
|
@ -31,8 +32,19 @@ function ResultsFooter({state, onClose}) {
|
|||
);
|
||||
}
|
||||
|
||||
function DocSearch(props) {
|
||||
function DocSearch({contextualSearch, ...props}) {
|
||||
const {siteMetadata} = useDocusaurusContext();
|
||||
|
||||
const contextualSearchParameters = useAlgoliaContextualSearchParameters();
|
||||
|
||||
// we let user override default searchParameters if he wants to
|
||||
const searchParameters = {
|
||||
...(contextualSearch ? contextualSearchParameters : {}),
|
||||
...props.searchParameters,
|
||||
};
|
||||
|
||||
console.log('searchParameters', contextualSearch, searchParameters);
|
||||
|
||||
const {withBaseUrl} = useBaseUrlUtils();
|
||||
const history = useHistory();
|
||||
const searchButtonRef = useRef(null);
|
||||
|
@ -152,6 +164,7 @@ function DocSearch(props) {
|
|||
resultsFooterComponent={resultsFooterComponent}
|
||||
transformSearchClient={transformSearchClient}
|
||||
{...props}
|
||||
searchParameters={searchParameters}
|
||||
/>,
|
||||
document.body,
|
||||
)}
|
||||
|
|
|
@ -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.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Head from '@docusaurus/Head';
|
||||
|
||||
// Override default/agnostic SearchMetas to use Algolia-specific metadatas
|
||||
export default function AlgoliaSearchMetadatas({language, version, tag}) {
|
||||
return (
|
||||
<Head>
|
||||
{language && <meta name="docsearch:language" content={`${language}`} />}
|
||||
{version && <meta name="docsearch:version" content={version} />}
|
||||
{tag && <meta name="docsearch:docusaurus_tag" content={tag} />}
|
||||
</Head>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
/**
|
||||
* 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.
|
||||
*/
|
||||
|
||||
import useContextualSearchFilters from '@theme/hooks/useContextualSearchFilters';
|
||||
|
||||
// Translate search-engine agnostic seach filters to Algolia search filters
|
||||
export function useAlgoliaContextualSearchParameters() {
|
||||
const {language, tags} = useContextualSearchFilters();
|
||||
|
||||
const languageFilter = `language:${language}`;
|
||||
|
||||
const tagsFilter = tags.map((tag) => `docusaurus_tag:${tag}`);
|
||||
|
||||
return {facetFilters: [languageFilter, tagsFilter]};
|
||||
}
|
|
@ -8,17 +8,28 @@
|
|||
const Joi = require('@hapi/joi');
|
||||
|
||||
const DEFAULT_CONFIG = {
|
||||
contextualSearch: false, // future: maybe we want to enable this by default
|
||||
|
||||
// By default, all Docusaurus sites are using the same AppId
|
||||
// This has been designed on purpose with Algolia.
|
||||
appId: 'BH4D9OD16A',
|
||||
|
||||
searchParameters: {},
|
||||
};
|
||||
exports.DEFAULT_CONFIG = DEFAULT_CONFIG;
|
||||
|
||||
const Schema = Joi.object({
|
||||
algolia: Joi.object({
|
||||
// Docusaurus attributes
|
||||
contextualSearch: Joi.boolean().default(DEFAULT_CONFIG.contextualSearch),
|
||||
|
||||
// Algolia attributes
|
||||
appId: Joi.string().default(DEFAULT_CONFIG.appId),
|
||||
apiKey: Joi.string().required(),
|
||||
indexName: Joi.string().required(),
|
||||
searchParameters: Joi.object()
|
||||
.default(DEFAULT_CONFIG.searchParameters)
|
||||
.unknown(),
|
||||
})
|
||||
.label('themeConfig.algolia')
|
||||
.required()
|
||||
|
@ -30,5 +41,17 @@ exports.validateThemeConfig = function validateThemeConfig({
|
|||
validate,
|
||||
themeConfig,
|
||||
}) {
|
||||
return validate(Schema, themeConfig);
|
||||
const normalizedThemeConfig = validate(Schema, themeConfig);
|
||||
|
||||
if (
|
||||
normalizedThemeConfig &&
|
||||
normalizedThemeConfig.algolia.contextualSearch &&
|
||||
normalizedThemeConfig.algolia.searchParameters &&
|
||||
normalizedThemeConfig.algolia.searchParameters.facetFilters
|
||||
) {
|
||||
throw new Error(
|
||||
'If you are using algolia.contextualSearch: true, you should not provide algolia.searchParameters.facetFilters, as it is computed for you dynamically',
|
||||
);
|
||||
}
|
||||
return normalizedThemeConfig;
|
||||
};
|
||||
|
|
|
@ -27,7 +27,14 @@ module.exports = {
|
|||
algolia: {
|
||||
apiKey: 'YOUR_API_KEY',
|
||||
indexName: 'YOUR_INDEX_NAME',
|
||||
searchParameters: {}, // Optional (if provided by Algolia)
|
||||
|
||||
// Optional: see doc section bellow
|
||||
contextualSearch: true,
|
||||
|
||||
// Optional: Algolia search parameters
|
||||
searchParameters: {},
|
||||
|
||||
//... other Algolia params
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
|
@ -40,6 +47,37 @@ The `searchParameters` option used to be named `algoliaOptions` in Docusaurus v1
|
|||
|
||||
:::
|
||||
|
||||
### Contextual search
|
||||
|
||||
Contextual search is mostly useful for versioned Docusaurus sites.
|
||||
|
||||
Let's consider you have 2 docs versions, v1 and v2. When you are browsing v2 docs, it would be odd to return search results for the v1 documentation. Sometimes v1 and v2 docs are quite similar, and you would end up with duplicate search results for the same query (one result per version).
|
||||
|
||||
To solve this problem, the contextual search feature understands that you are browsing a specific docs version, and will create the search query filters dynamically.
|
||||
|
||||
- browsing `/docs/v1/myDoc`, search results will only include **v1** docs (+ other unversioned pages)
|
||||
- browsing `/docs/v2/myDoc`, search results will only include **v2** docs (+ other unversioned pages)
|
||||
|
||||
```jsx title="docusaurus.config.js"
|
||||
module.exports = {
|
||||
// ...
|
||||
themeConfig: {
|
||||
// ...
|
||||
// highlight-start
|
||||
algolia: {
|
||||
contextualSearch: true,
|
||||
},
|
||||
// highlight-end
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
:::caution
|
||||
|
||||
If you decide to use contextual search, you can't provide `algolia.searchParameters.facetFilters`, as we compute this `facetFilters` attribute for you dynamically.
|
||||
|
||||
:::
|
||||
|
||||
### Styling your Algolia search
|
||||
|
||||
By default, DocSearch comes with a fine-tuned theme that was designed for accessibility, making sure that colors and contrasts respect standards.
|
||||
|
|
|
@ -249,8 +249,9 @@ module.exports = {
|
|||
algolia: {
|
||||
apiKey: '47ecd3b21be71c5822571b9f59e52544',
|
||||
indexName: 'docusaurus-2',
|
||||
// contextualSearch: true,
|
||||
searchParameters: {
|
||||
facetFilters: [`version:latest`],
|
||||
facetFilters: [`version:current`],
|
||||
},
|
||||
},
|
||||
navbar: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue