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