From e32dcacd7bd51a03510444df65fc406195018c6b Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Fri, 13 Dec 2024 10:54:19 -0800 Subject: [PATCH 1/5] swizzle search, internal only links --- docusaurus.config.js | 22 +-- package.json | 1 + src/theme/SearchBar/index.tsx | 279 +++++++++++++++++++++++++++++++++ src/theme/SearchBar/styles.css | 14 ++ yarn.lock | 2 +- 5 files changed, 302 insertions(+), 16 deletions(-) create mode 100644 src/theme/SearchBar/index.tsx create mode 100644 src/theme/SearchBar/styles.css diff --git a/docusaurus.config.js b/docusaurus.config.js index 2ced3fc3..97eaf20f 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -120,21 +120,13 @@ const config = { theme: require("./src/internals/prism-github"), darkTheme: require("./src/internals/prism-dracula"), }, - - ...(typeof process.env.ALGOLIA_API_KEY === "string" && - typeof process.env.ALGOLIA_APP_ID === "string" - ? { - algolia: { - appId: process.env.ALGOLIA_APP_ID, - apiKey: process.env.ALGOLIA_API_KEY, - indexName: "questdb", - // Disable /search page - searchPagePath: false, - contextualSearch: false, - externalUrlRegex: 'questdb\\.io', - }, - } - : {}), + algolia: { + appId: process.env.ALGOLIA_APP_ID || 'placeholder-app-id', + apiKey: process.env.ALGOLIA_API_KEY || 'placeholder-api-key', + indexName: "questdb", + searchPagePath: false, + contextualSearch: false, + }, }, presets: [ [ diff --git a/package.json b/package.json index 67b9d088..3fd1cf52 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@docusaurus/faster": "^3.6.3", "@docusaurus/theme-mermaid": "^3.6.3", + "@docusaurus/theme-search-algolia": "^3.6.3", "@headlessui/react": "^2.2.0", "@heroicons/react": "2.2.0", "@mdx-js/react": "3.1.0", diff --git a/src/theme/SearchBar/index.tsx b/src/theme/SearchBar/index.tsx new file mode 100644 index 00000000..163d89dc --- /dev/null +++ b/src/theme/SearchBar/index.tsx @@ -0,0 +1,279 @@ +import React, {useCallback, useMemo, useRef, useState} from 'react'; +import {createPortal} from 'react-dom'; +import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react'; +import Head from '@docusaurus/Head'; +import Link from '@docusaurus/Link'; +import {useHistory} from '@docusaurus/router'; +import { + isRegexpStringMatch, + useSearchLinkCreator, +} from '@docusaurus/theme-common'; +import { + useAlgoliaContextualFacetFilters, + useSearchResultUrlProcessor, +} from '@docusaurus/theme-search-algolia/client'; +import Translate from '@docusaurus/Translate'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import translations from '@theme/SearchTranslations'; + +import type {AutocompleteState} from '@algolia/autocomplete-core'; +import type { + DocSearchModal as DocSearchModalType, + DocSearchModalProps, +} from '@docsearch/react'; +import type { + InternalDocSearchHit, + StoredDocSearchHit, +} from '@docsearch/react/dist/esm/types'; +import type {SearchClient} from 'algoliasearch/lite'; + +type DocSearchProps = Omit< + DocSearchModalProps, + 'onClose' | 'initialScrollY' +> & { + contextualSearch?: string; + externalUrlRegex?: string; + searchPagePath: boolean | string; +}; + +let DocSearchModal: typeof DocSearchModalType | null = null; + +function Hit({ + hit, + children, +}: { + hit: InternalDocSearchHit | StoredDocSearchHit; + children: React.ReactNode; +}) { + const history = useHistory(); + + const handleClick = (e: React.MouseEvent) => { + e.preventDefault(); + history.push(hit.url); + }; + + return ( + + {children} + + ); +} + +type ResultsFooterProps = { + state: AutocompleteState; + onClose: () => void; +}; + +function ResultsFooter({state, onClose}: ResultsFooterProps) { + const createSearchLink = useSearchLinkCreator(); + + return ( + + + {'See all {count} results'} + + + ); +} + +type FacetFilters = Required< + Required['searchParameters'] +>['facetFilters']; + +function mergeFacetFilters(f1: FacetFilters, f2: FacetFilters): FacetFilters { + const normalize = ( + f: FacetFilters, + ): readonly string[] | readonly (string | readonly string[])[] => + typeof f === 'string' ? [f] : f; + return [...normalize(f1), ...normalize(f2)] as FacetFilters; +} + +function DocSearch({ + contextualSearch, + externalUrlRegex, + ...props +}: DocSearchProps) { + const {siteMetadata} = useDocusaurusContext(); + const processSearchResultUrl = useSearchResultUrlProcessor(); + + const contextualSearchFacetFilters = + useAlgoliaContextualFacetFilters() as FacetFilters; + + const configFacetFilters: FacetFilters = + props.searchParameters?.facetFilters ?? []; + + const facetFilters: FacetFilters = contextualSearch + ? // Merge contextual search filters with config filters + mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters) + : // ... or use config facetFilters + configFacetFilters; + + // We let user override default searchParameters if she wants to + const searchParameters: DocSearchProps['searchParameters'] = { + ...props.searchParameters, + facetFilters, + }; + + const history = useHistory(); + const searchContainer = useRef(null); + const searchButtonRef = useRef(null); + const [isOpen, setIsOpen] = useState(false); + const [initialQuery, setInitialQuery] = useState( + undefined, + ); + + const importDocSearchModalIfNeeded = useCallback(() => { + if (DocSearchModal) { + return Promise.resolve(); + } + + return Promise.all([ + import('@docsearch/react/modal') as Promise< + typeof import('@docsearch/react') + >, + import('@docsearch/react/style'), + import('./styles.css'), + ]).then(([{DocSearchModal: Modal}]) => { + DocSearchModal = Modal; + }); + }, []); + + const prepareSearchContainer = useCallback(() => { + if (!searchContainer.current) { + const divElement = document.createElement('div'); + searchContainer.current = divElement; + document.body.insertBefore(divElement, document.body.firstChild); + } + }, []); + + const openModal = useCallback(() => { + prepareSearchContainer(); + importDocSearchModalIfNeeded().then(() => setIsOpen(true)); + }, [importDocSearchModalIfNeeded, prepareSearchContainer]); + + const closeModal = useCallback(() => { + setIsOpen(false); + searchButtonRef.current?.focus(); + }, []); + + const handleInput = useCallback( + (event: KeyboardEvent) => { + if (event.key === 'f' && (event.metaKey || event.ctrlKey)) { + // ignore browser's ctrl+f + return; + } + // prevents duplicate key insertion in the modal input + event.preventDefault(); + setInitialQuery(event.key); + openModal(); + }, + [openModal], + ); + + const navigator = useRef({ + navigate({itemUrl}: {itemUrl?: string}) { + // 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) => + props.transformItems + ? // Custom transformItems + props.transformItems(items) + : // Default transformItems + items.map((item) => ({ + ...item, + url: processSearchResultUrl(item.url), + })), + ).current; + + const resultsFooterComponent: DocSearchProps['resultsFooterComponent'] = + useMemo( + () => + // eslint-disable-next-line react/no-unstable-nested-components + (footerProps: Omit): JSX.Element => + , + [closeModal], + ); + + const transformSearchClient = useCallback( + (searchClient: SearchClient) => { + searchClient.addAlgoliaAgent( + 'docusaurus', + siteMetadata.docusaurusVersion, + ); + + return searchClient; + }, + [siteMetadata.docusaurusVersion], + ); + + useDocSearchKeyboardEvents({ + isOpen, + onOpen: openModal, + onClose: closeModal, + onInput: handleInput, + searchButtonRef, + }); + + return ( + <> + + {/* This hints the browser that the website will load data from Algolia, + and allows it to preconnect to the DocSearch cluster. It makes the first + query faster, especially on mobile. */} + + + + + + {isOpen && + DocSearchModal && + searchContainer.current && + createPortal( + , + searchContainer.current, + )} + + ); +} + +export default function SearchBar(): JSX.Element { + const {siteConfig} = useDocusaurusContext(); + return ; +} diff --git a/src/theme/SearchBar/styles.css b/src/theme/SearchBar/styles.css new file mode 100644 index 00000000..fdf8dff9 --- /dev/null +++ b/src/theme/SearchBar/styles.css @@ -0,0 +1,14 @@ +:root { + --docsearch-primary-color: var(--ifm-color-primary); + --docsearch-text-color: var(--ifm-font-color-base); +} + +.DocSearch-Button { + margin: 0; + transition: all var(--ifm-transition-fast) + var(--ifm-transition-timing-default); +} + +.DocSearch-Container { + z-index: calc(var(--ifm-z-index-fixed) + 1); +} diff --git a/yarn.lock b/yarn.lock index 5d65c4af..0b72e9a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1999,7 +1999,7 @@ mermaid ">=10.4" tslib "^2.6.0" -"@docusaurus/theme-search-algolia@3.6.3": +"@docusaurus/theme-search-algolia@3.6.3", "@docusaurus/theme-search-algolia@^3.6.3": version "3.6.3" resolved "https://registry.yarnpkg.com/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.6.3.tgz#1a3331a489f392f5b032c4efc5f431e57eddf7ce" integrity sha512-rt+MGCCpYgPyWCGXtbxlwFbTSobu15jWBTPI2LHsHNa5B0zSmOISX6FWYAPt5X1rNDOqMGM0FATnh7TBHRohVA== From be6d9fbe5c3dadddff92774a023182dcea5f5504 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:08:31 -0800 Subject: [PATCH 2/5] try to be relative --- src/theme/SearchBar/index.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/theme/SearchBar/index.tsx b/src/theme/SearchBar/index.tsx index 163d89dc..2b2e2025 100644 --- a/src/theme/SearchBar/index.tsx +++ b/src/theme/SearchBar/index.tsx @@ -45,11 +45,12 @@ function Hit({ hit: InternalDocSearchHit | StoredDocSearchHit; children: React.ReactNode; }) { - const history = useHistory(); - const handleClick = (e: React.MouseEvent) => { e.preventDefault(); - history.push(hit.url); + // Remove any domain part from the URL if it exists + const url = hit.url.replace(/^https?:\/\/[^\/]+/, ''); + // Navigate to the URL in the same tab + window.location.assign(url); }; return ( From c1b5531dcabdeab06f276581b7dfdf7e9c558f63 Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:14:09 -0800 Subject: [PATCH 3/5] debug logging --- src/theme/SearchBar/index.tsx | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/theme/SearchBar/index.tsx b/src/theme/SearchBar/index.tsx index 2b2e2025..e11fd2b6 100644 --- a/src/theme/SearchBar/index.tsx +++ b/src/theme/SearchBar/index.tsx @@ -47,9 +47,24 @@ function Hit({ }) { const handleClick = (e: React.MouseEvent) => { e.preventDefault(); - // Remove any domain part from the URL if it exists - const url = hit.url.replace(/^https?:\/\/[^\/]+/, ''); - // Navigate to the URL in the same tab + + // Log the original URL for debugging + console.log('Original URL:', hit.url); + + // 1. Remove domain if present + let url = hit.url.replace(/^https?:\/\/[^\/]+/, ''); + console.log('After domain removal:', url); + + // 2. Handle double /docs/ if present (in case the URL already has /docs/ and gets another prepended) + url = url.replace(/^\/docs\/docs\//, '/docs/'); + console.log('After fixing double docs:', url); + + // 3. Handle case where /docs/ is missing from documentation pages + if (!url.startsWith('/docs/') && !url.match(/^\/(glossary|blog)/)) { + url = `/docs${url}`; + } + console.log('Final URL:', url); + window.location.assign(url); }; From d6ede186dd03b6d150492309e0dfe97adb1cec5e Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:19:34 -0800 Subject: [PATCH 4/5] tidy specific urls --- src/theme/SearchBar/index.tsx | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/theme/SearchBar/index.tsx b/src/theme/SearchBar/index.tsx index e11fd2b6..0e656074 100644 --- a/src/theme/SearchBar/index.tsx +++ b/src/theme/SearchBar/index.tsx @@ -48,22 +48,13 @@ function Hit({ const handleClick = (e: React.MouseEvent) => { e.preventDefault(); - // Log the original URL for debugging - console.log('Original URL:', hit.url); - // 1. Remove domain if present let url = hit.url.replace(/^https?:\/\/[^\/]+/, ''); - console.log('After domain removal:', url); - - // 2. Handle double /docs/ if present (in case the URL already has /docs/ and gets another prepended) - url = url.replace(/^\/docs\/docs\//, '/docs/'); - console.log('After fixing double docs:', url); - // 3. Handle case where /docs/ is missing from documentation pages - if (!url.startsWith('/docs/') && !url.match(/^\/(glossary|blog)/)) { - url = `/docs${url}`; + // 2. For glossary/blog URLs, ensure they don't have /docs/ prefix + if (url.match(/\/docs\/(glossary|blog)/)) { + url = url.replace('/docs/', '/'); } - console.log('Final URL:', url); window.location.assign(url); }; From 440314146ed9682008152ce5b5d527aabe537c4a Mon Sep 17 00:00:00 2001 From: goodroot <9484709+goodroot@users.noreply.github.com> Date: Fri, 13 Dec 2024 11:29:02 -0800 Subject: [PATCH 5/5] tidy implementation, raise higher in order yay first try.gif --- src/theme/SearchBar/index.tsx | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/theme/SearchBar/index.tsx b/src/theme/SearchBar/index.tsx index 0e656074..5fcd2988 100644 --- a/src/theme/SearchBar/index.tsx +++ b/src/theme/SearchBar/index.tsx @@ -45,22 +45,22 @@ function Hit({ hit: InternalDocSearchHit | StoredDocSearchHit; children: React.ReactNode; }) { + // Transform URL once and use it in both places + const transformUrl = (url: string) => { + return (url.startsWith('/docs/glossary/') || url.startsWith('/docs/blog/')) + ? url.replace('/docs/', '/') + : url; + }; + + const finalUrl = transformUrl(hit.url); + const handleClick = (e: React.MouseEvent) => { e.preventDefault(); - - // 1. Remove domain if present - let url = hit.url.replace(/^https?:\/\/[^\/]+/, ''); - - // 2. For glossary/blog URLs, ensure they don't have /docs/ prefix - if (url.match(/\/docs\/(glossary|blog)/)) { - url = url.replace('/docs/', '/'); - } - - window.location.assign(url); + window.location.assign(finalUrl); }; return ( - + {children} );