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/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 a1f87ae6c56..7df28493e0b 100644 --- a/src/theme/SearchBar/utils/searchConfig.js +++ b/src/theme/SearchBar/utils/searchConfig.js @@ -60,15 +60,73 @@ 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; + + 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); + 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 { + pathname = itemUrl; + hash = ''; + } + } + + // 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, prepend the locale-specific baseUrl + // This is needed because history.push expects the full path including baseUrl + 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)) { + window.location.href = url; } else { - history.push(itemUrl); + history.push(url); } }, }; @@ -143,7 +201,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