From cdbd7a7c089240757312d5ba1517a9008f119c61 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Mon, 1 Dec 2025 15:47:08 +0100 Subject: [PATCH 1/5] fix URL issue when pressing enter --- src/theme/SearchBar/index.js | 106 +++++++++++----------- src/theme/SearchBar/utils/searchConfig.js | 40 +++++++- 2 files changed, 89 insertions(+), 57 deletions(-) diff --git a/src/theme/SearchBar/index.js b/src/theme/SearchBar/index.js index 595140b1c5e..646fcc0dcd7 100644 --- a/src/theme/SearchBar/index.js +++ b/src/theme/SearchBar/index.js @@ -9,14 +9,14 @@ import { import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import { createPortal } from 'react-dom'; import translations from '@theme/SearchTranslations'; -import {useAskAI} from '@site/src/hooks/useAskAI' +import { useAskAI } from '@site/src/hooks/useAskAI' import { shouldPreventSearchAction, handleSearchKeyboardConflict } from './utils/aiConflictHandler'; import { initializeSearchAnalytics, createEnhancedSearchClient } from './utils/searchAnalytics'; import { useDocSearchModal } from './utils/useDocSearchModal'; -import { - createSearchParameters, - createSearchNavigator, - transformSearchItems +import { + createSearchParameters, + createSearchNavigator, + transformSearchItems } from './utils/searchConfig'; import { SearchHit } from './searchHit'; import { SearchResultsFooter } from './searchResultsFooter'; @@ -31,10 +31,10 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { const { isAskAIOpen } = useAskAI(); const history = useHistory(); const searchButtonRef = useRef(null); - + const [selectedDocTypes, setSelectedDocTypes] = useState(null); const searchParametersRef = useRef(null); - + const { isOpen, initialQuery, @@ -49,12 +49,12 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { // Update searchParameters ref instead of creating new object useEffect(() => { const newParams = createSearchParameters( - props, - contextualSearch, + props, + contextualSearch, contextualSearchFacetFilters, selectedDocTypes ); - + if (!searchParametersRef.current) { searchParametersRef.current = newParams; } else { @@ -67,8 +67,8 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { // Initialize on mount if (!searchParametersRef.current) { searchParametersRef.current = createSearchParameters( - props, - contextualSearch, + props, + contextualSearch, contextualSearchFacetFilters, selectedDocTypes ); @@ -77,14 +77,14 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { // Track input changes to capture the query useEffect(() => { if (!isOpen) return; - + const handleInput = (e) => { const input = e.target; if (input.classList.contains('DocSearch-Input')) { lastQueryRef.current = input.value; } }; - + document.addEventListener('input', handleInput, true); return () => document.removeEventListener('input', handleInput, true); }, [isOpen]); @@ -94,15 +94,15 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { }, [props.appId, props.apiKey]); const navigator = useMemo( - () => createSearchNavigator(history, externalUrlRegex), - [history, externalUrlRegex] + () => createSearchNavigator(history, externalUrlRegex, currentLocale), + [history, externalUrlRegex, currentLocale] ); const transformItems = useCallback((items, state) => { if (state?.query) { lastQueryRef.current = state.query; } - + return transformSearchItems(items, { transformItems: props.transformItems, processSearchResultUrl, @@ -111,34 +111,34 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) { }); }, [props.transformItems, processSearchResultUrl, currentLocale]); -const handleDocTypeChange = useCallback((docTypes) => { - setSelectedDocTypes(docTypes); - - // Re-trigger search with updated filters after state update completes - setTimeout(() => { - const input = document.querySelector('.DocSearch-Input'); - const query = lastQueryRef.current; - - if (input && query) { - // Access React's internal value setter to bypass readonly property - const nativeInputValueSetter = Object.getOwnPropertyDescriptor( - window.HTMLInputElement.prototype, - 'value' - ).set; - - // Clear input to trigger change detection - nativeInputValueSetter.call(input, ''); - input.dispatchEvent(new Event('input', { bubbles: true })); - - // Restore original query to execute search with new filters - setTimeout(() => { - nativeInputValueSetter.call(input, query); + const handleDocTypeChange = useCallback((docTypes) => { + setSelectedDocTypes(docTypes); + + // Re-trigger search with updated filters after state update completes + setTimeout(() => { + const input = document.querySelector('.DocSearch-Input'); + const query = lastQueryRef.current; + + if (input && query) { + // Access React's internal value setter to bypass readonly property + const nativeInputValueSetter = Object.getOwnPropertyDescriptor( + window.HTMLInputElement.prototype, + 'value' + ).set; + + // Clear input to trigger change detection + nativeInputValueSetter.call(input, ''); input.dispatchEvent(new Event('input', { bubbles: true })); - input.focus(); - }, 0); - } - }, 100); -}, []); + + // Restore original query to execute search with new filters + setTimeout(() => { + nativeInputValueSetter.call(input, query); + input.dispatchEvent(new Event('input', { bubbles: true })); + input.focus(); + }, 0); + } + }, 100); + }, []); const resultsFooterComponent = useMemo( () => (footerProps) => , @@ -147,13 +147,13 @@ const handleDocTypeChange = useCallback((docTypes) => { const transformSearchClient = useCallback((searchClient) => { const enhancedClient = createEnhancedSearchClient( - searchClient, - siteMetadata.docusaurusVersion, + searchClient, + siteMetadata.docusaurusVersion, queryIDRef ); - + const originalSearch = enhancedClient.search.bind(enhancedClient); - + let debounceTimeout; enhancedClient.search = (...args) => { return new Promise((resolve, reject) => { @@ -165,7 +165,7 @@ const handleDocTypeChange = useCallback((docTypes) => { }, 200); }); }; - + return enhancedClient; }, [siteMetadata.docusaurusVersion]); @@ -190,7 +190,7 @@ const handleDocTypeChange = useCallback((docTypes) => { onInput: handleOnInput, searchButtonRef, }); - + return ( <> @@ -214,7 +214,7 @@ const handleDocTypeChange = useCallback((docTypes) => { DocSearchModal && searchContainer && createPortal( - <> + <> { placeholder={translations.placeholder} translations={translations.modal} /> - +
{ backgroundColor: 'var(--docsearch-modal-background)', boxShadow: '0 2px 8px rgba(0,0,0,0.1)' }}> - diff --git a/src/theme/SearchBar/utils/searchConfig.js b/src/theme/SearchBar/utils/searchConfig.js index 7dbf4e22ff8..9c1e92c55fe 100644 --- a/src/theme/SearchBar/utils/searchConfig.js +++ b/src/theme/SearchBar/utils/searchConfig.js @@ -60,15 +60,47 @@ export function createSearchParameters(props, contextualSearch, contextualSearch * Create navigator for handling search result clicks * @param {Object} history - React router history object * @param {RegExp} externalUrlRegex - Regex to match external URLs + * @param {string} currentLocale - Current locale * @returns {Object} - Navigator object */ -export function createSearchNavigator(history, externalUrlRegex) { +export function createSearchNavigator(history, externalUrlRegex, currentLocale) { return { navigate({ itemUrl }) { - if (isRegexpStringMatch(externalUrlRegex, itemUrl)) { - window.location.href = itemUrl; + let url = itemUrl; + + // Transform the URL if it's an absolute URL from Algolia + // This handles the case when Enter is pressed (which may receive untransformed URLs) + try { + const urlObj = new URL(itemUrl); + const pathname = urlObj.pathname; + const hash = urlObj.hash; + + if (currentLocale !== 'en') { + const prefix = `/docs/${currentLocale}`; + if (pathname.startsWith(prefix)) { + url = pathname.substring(prefix.length) || '/'; + } else { + url = pathname; + } + } else { + const prefix = '/docs'; + if (pathname.startsWith(prefix)) { + url = pathname.substring(prefix.length) || '/'; + } else { + url = pathname; + } + } + + url += hash; + } catch (e) { + // If URL parsing fails, it's likely already a relative path + // Use it as-is + } + + if (isRegexpStringMatch(externalUrlRegex, url)) { + window.location.href = url; } else { - history.push(itemUrl); + history.push(url); } }, }; From 692c32808f9926fa2853c482f0afbca5654ec650 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Mon, 1 Dec 2025 16:47:00 +0100 Subject: [PATCH 2/5] check for relative paths --- src/theme/SearchBar/utils/searchConfig.js | 51 ++++++++++++----------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/src/theme/SearchBar/utils/searchConfig.js b/src/theme/SearchBar/utils/searchConfig.js index ef688849ec1..e4a8fb4b82c 100644 --- a/src/theme/SearchBar/utils/searchConfig.js +++ b/src/theme/SearchBar/utils/searchConfig.js @@ -68,34 +68,37 @@ export function createSearchNavigator(history, externalUrlRegex, currentLocale) navigate({ itemUrl }) { let url = itemUrl; - // Transform the URL if it's an absolute URL from Algolia - // This handles the case when Enter is pressed (which may receive untransformed URLs) - try { - const urlObj = new URL(itemUrl); - const pathname = urlObj.pathname; - const hash = urlObj.hash; - - if (currentLocale !== 'en') { - const prefix = `/docs/${currentLocale}`; - if (pathname.startsWith(prefix)) { - url = pathname.substring(prefix.length) || '/'; + // Only transform if it's an absolute URL (starts with http:// or https://) + // If it's already a relative path, it's been transformed by transformSearchItems + if (itemUrl.startsWith('http://') || itemUrl.startsWith('https://')) { + // Transform the absolute URL from Algolia to a relative path + try { + const urlObj = new URL(itemUrl); + const pathname = urlObj.pathname; + const hash = urlObj.hash; + + if (currentLocale !== 'en') { + const prefix = `/docs/${currentLocale}`; + if (pathname.startsWith(prefix)) { + url = pathname.substring(prefix.length) || '/'; + } else { + url = pathname; + } } else { - url = pathname; + const prefix = '/docs'; + if (pathname.startsWith(prefix)) { + url = pathname.substring(prefix.length) || '/'; + } else { + url = pathname; + } } - } else { - const prefix = '/docs'; - if (pathname.startsWith(prefix)) { - url = pathname.substring(prefix.length) || '/'; - } else { - url = pathname; - } - } - url += hash; - } catch (e) { - // If URL parsing fails, it's likely already a relative path - // Use it as-is + url += hash; + } catch (e) { + // If parsing fails, use as-is + } } + // else: URL is already relative (transformed), use as-is if (isRegexpStringMatch(externalUrlRegex, url)) { window.location.href = url; From 580c7d68735156f22225d85dd7c6976d61703145 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:07:41 +0100 Subject: [PATCH 3/5] fix issue with baseTransform --- src/theme/SearchBar/utils/searchConfig.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/theme/SearchBar/utils/searchConfig.js b/src/theme/SearchBar/utils/searchConfig.js index e4a8fb4b82c..18eb3f43536 100644 --- a/src/theme/SearchBar/utils/searchConfig.js +++ b/src/theme/SearchBar/utils/searchConfig.js @@ -178,7 +178,11 @@ export function transformSearchItems(items, options) { return transformed; }); - const result = transformItems ? transformItems(items) : baseTransform(items); + // Always apply base transformation first to fix URLs + const baseTransformed = baseTransform(items); + + // Then optionally apply custom transformation on top + const result = transformItems ? transformItems(baseTransformed) : baseTransformed; return result; } \ No newline at end of file From 056f71d9e860d899e7c60dabf5a6a1a7808f6710 Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Mon, 1 Dec 2025 17:53:09 +0100 Subject: [PATCH 4/5] fix --- src/theme/SearchBar/searchHit.jsx | 66 +++++++++++++++++++--- src/theme/SearchBar/utils/searchConfig.js | 67 ++++++++++++++--------- 2 files changed, 99 insertions(+), 34 deletions(-) diff --git a/src/theme/SearchBar/searchHit.jsx b/src/theme/SearchBar/searchHit.jsx index 3eed981e57c..e4fd9dd82f7 100644 --- a/src/theme/SearchBar/searchHit.jsx +++ b/src/theme/SearchBar/searchHit.jsx @@ -1,15 +1,63 @@ import Link from '@docusaurus/Link'; import { trackSearchResultClick } from './utils/searchAnalytics'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; export function SearchHit({ hit, children }) { + const { i18n: { currentLocale } } = useDocusaurusContext(); const handleClick = () => trackSearchResultClick(hit); - + + // Transform the URL to ensure it's correct + // This is a safety measure in case transformSearchItems doesn't work + let transformedUrl = hit.url; + + try { + let pathname, hash; + + // If it's an absolute URL, extract pathname and hash + if (hit.url.startsWith('http://') || hit.url.startsWith('https://')) { + const urlObj = new URL(hit.url); + pathname = urlObj.pathname; + hash = urlObj.hash; + } else { + // It's already a relative URL, split pathname and hash + const hashIndex = hit.url.indexOf('#'); + if (hashIndex !== -1) { + pathname = hit.url.substring(0, hashIndex); + hash = hit.url.substring(hashIndex); + } else { + pathname = hit.url; + hash = ''; + } + } + + // Now transform the pathname + if (currentLocale !== 'en') { + const prefix = `/docs/${currentLocale}`; + if (pathname.startsWith(prefix)) { + transformedUrl = pathname.substring(prefix.length) || '/'; + } else { + transformedUrl = pathname; + } + } else { + const prefix = '/docs'; + if (pathname.startsWith(prefix)) { + transformedUrl = pathname.substring(prefix.length) || '/'; + } else { + transformedUrl = pathname; + } + } + + transformedUrl += hash; + } catch (e) { + // If transformation fails, use original URL + } + // Extract multiple URL segments after /docs/ and clean them up const segments = hit.url.split('/docs/')[1]?.split('/').filter(Boolean) || []; const breadcrumbs = segments .slice(0, 3) // Take first 3 segments max .map(segment => segment.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase())); - + // Format doc_type for display, stripping quotes and formatting const formatDocType = (docType) => { if (!docType) return null; @@ -17,21 +65,21 @@ export function SearchHit({ hit, children }) { const cleaned = docType.replace(/^'|'$/g, ''); return cleaned.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase()); }; - + const docTypeDisplay = formatDocType(hit.doc_type); - + return ( - + {children} -
{/* Doc type badge */} {docTypeDisplay && ( - )} - + {/* Breadcrumbs */} {breadcrumbs.length > 0 && ( {breadcrumbs.join(' › ')} diff --git a/src/theme/SearchBar/utils/searchConfig.js b/src/theme/SearchBar/utils/searchConfig.js index 18eb3f43536..8570af48dc6 100644 --- a/src/theme/SearchBar/utils/searchConfig.js +++ b/src/theme/SearchBar/utils/searchConfig.js @@ -68,37 +68,54 @@ export function createSearchNavigator(history, externalUrlRegex, currentLocale) navigate({ itemUrl }) { let url = itemUrl; - // Only transform if it's an absolute URL (starts with http:// or https://) - // If it's already a relative path, it's been transformed by transformSearchItems - if (itemUrl.startsWith('http://') || itemUrl.startsWith('https://')) { - // Transform the absolute URL from Algolia to a relative path - try { + try { + let pathname, hash; + + // Handle both absolute and relative URLs + if (itemUrl.startsWith('http://') || itemUrl.startsWith('https://')) { + // Absolute URL - parse it const urlObj = new URL(itemUrl); - const pathname = urlObj.pathname; - const hash = urlObj.hash; - - if (currentLocale !== 'en') { - const prefix = `/docs/${currentLocale}`; - if (pathname.startsWith(prefix)) { - url = pathname.substring(prefix.length) || '/'; - } else { - url = pathname; - } + pathname = urlObj.pathname; + hash = urlObj.hash; + } else { + // Relative URL - split pathname and hash manually + const hashIndex = itemUrl.indexOf('#'); + if (hashIndex !== -1) { + pathname = itemUrl.substring(0, hashIndex); + hash = itemUrl.substring(hashIndex); } else { - const prefix = '/docs'; - if (pathname.startsWith(prefix)) { - url = pathname.substring(prefix.length) || '/'; - } else { - url = pathname; - } + pathname = itemUrl; + hash = ''; } + } - url += hash; - } catch (e) { - // If parsing fails, use as-is + // Transform the pathname if it starts with /docs + if (currentLocale !== 'en') { + const prefix = `/docs/${currentLocale}`; + if (pathname.startsWith(prefix)) { + url = pathname.substring(prefix.length) || '/'; + } else { + url = pathname; + } + } else { + const prefix = '/docs'; + if (pathname.startsWith(prefix)) { + url = pathname.substring(prefix.length) || '/'; + } else { + url = pathname; + } } + + url += hash; + } catch (e) { + // If parsing fails, use as-is + } + + // If the URL is relative and doesn't already start with /docs, prepend it + // This is needed because history.push expects the full path including baseUrl + if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('/docs')) { + url = '/docs' + url; } - // else: URL is already relative (transformed), use as-is if (isRegexpStringMatch(externalUrlRegex, url)) { window.location.href = url; From 7e44437468d064600d5c61e3335d06f143a3f32b Mon Sep 17 00:00:00 2001 From: Shaun Struwig <41984034+Blargian@users.noreply.github.com> Date: Mon, 1 Dec 2025 18:21:42 +0100 Subject: [PATCH 5/5] fix for multilingual search --- src/theme/SearchBar/utils/searchConfig.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/theme/SearchBar/utils/searchConfig.js b/src/theme/SearchBar/utils/searchConfig.js index 8570af48dc6..7df28493e0b 100644 --- a/src/theme/SearchBar/utils/searchConfig.js +++ b/src/theme/SearchBar/utils/searchConfig.js @@ -111,10 +111,16 @@ export function createSearchNavigator(history, externalUrlRegex, currentLocale) // If parsing fails, use as-is } - // If the URL is relative and doesn't already start with /docs, prepend it + // If the URL is relative, prepend the locale-specific baseUrl // This is needed because history.push expects the full path including baseUrl - if (!url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('/docs')) { - url = '/docs' + url; + if (!url.startsWith('http://') && !url.startsWith('https://')) { + // Construct the baseUrl based on locale + const baseUrl = currentLocale !== 'en' ? `/docs/${currentLocale}` : '/docs'; + + // Only prepend if the URL doesn't already start with the baseUrl + if (!url.startsWith(baseUrl)) { + url = baseUrl + url; + } } if (isRegexpStringMatch(externalUrlRegex, url)) {