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)) {