From 5f29808f93993401e3e5b95894163aec5ff34fe3 Mon Sep 17 00:00:00 2001 From: Christopher Hakkaart Date: Thu, 26 Feb 2026 16:51:23 +1300 Subject: [PATCH 1/8] Stable search page and results --- docusaurus.config.js | 7 +- src/theme/SearchPage/generalUtils.js | 23 ++ src/theme/SearchPage/index.js | 374 +++++++++++++++++++++++++ src/theme/SearchPage/styles.module.css | 132 +++++++++ src/theme/SearchPage/useSearchPage.js | 51 ++++ 5 files changed, 584 insertions(+), 3 deletions(-) create mode 100644 src/theme/SearchPage/generalUtils.js create mode 100644 src/theme/SearchPage/index.js create mode 100644 src/theme/SearchPage/styles.module.css create mode 100644 src/theme/SearchPage/useSearchPage.js diff --git a/docusaurus.config.js b/docusaurus.config.js index ef6c7c735..d1eacf858 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -295,13 +295,14 @@ export default async function createConfigAsync() { }, ], apiKey: process.env.TYPESENSE_SEARCH_API_KEY, - connectionTimeoutSeconds: 2, + connectionTimeoutSeconds: 5, // Default value }, typesenseSearchParameters: { query_by: 'content,hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3', - group_by: 'url_without_anchor', + group_by: 'url', group_limit: 1, - num_typos: 1, + per_page: 20, + num_typos: 2, prioritize_exact_match: true, filter_by: 'docusaurus_tag:!=[default,doc_tag_doc_list,blog_posts_list,blog_tags_posts,doc_tags_list,blog_tags_list]', // TODO Remove once the scraper is updated }, diff --git a/src/theme/SearchPage/generalUtils.js b/src/theme/SearchPage/generalUtils.js new file mode 100644 index 000000000..adb498600 --- /dev/null +++ b/src/theme/SearchPage/generalUtils.js @@ -0,0 +1,23 @@ +"use strict"; +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useTitleFormatter = useTitleFormatter; +const tslib_1 = require("tslib"); +// Source: https://github.com/facebook/docusaurus/blob/a308fb7c81832cca354192fe2984f52749441249/packages/docusaurus-theme-common/src/utils/generalUtils.ts +// Context: https://github.com/typesense/docusaurus-theme-search-typesense/issues/27#issuecomment-1415757477 +const useDocusaurusContext_1 = tslib_1.__importDefault(require("@docusaurus/useDocusaurusContext")); +/** + * Formats the page's title based on relevant site config and other contexts. + */ +function useTitleFormatter(title) { + const { siteConfig } = (0, useDocusaurusContext_1.default)(); + const { title: siteTitle, titleDelimiter } = siteConfig; + return title?.trim().length + ? `${title.trim()} ${titleDelimiter} ${siteTitle}` + : siteTitle; +} diff --git a/src/theme/SearchPage/index.js b/src/theme/SearchPage/index.js new file mode 100644 index 000000000..ea291f043 --- /dev/null +++ b/src/theme/SearchPage/index.js @@ -0,0 +1,374 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * Swizzled from docusaurus-theme-search-typesense to remove hardcoded + * group_by: 'url' so search page results match the search bar. + */ +/* eslint-disable jsx-a11y/no-autofocus */ +import React, { useEffect, useMemo, useState, useReducer, useRef } from 'react'; +import clsx from 'clsx'; +import algoliaSearchHelper from 'algoliasearch-helper'; +import Head from '@docusaurus/Head'; +import Link from '@docusaurus/Link'; +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; +import { HtmlClassNameProvider, usePluralForm, isRegexpStringMatch, useEvent, +// @ts-ignore + } from '@docusaurus/theme-common'; +import { useSearchPage } from './useSearchPage'; +// @ts-ignore +import { useTitleFormatter } from './generalUtils'; +import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; +import { useAllDocsData } from '@docusaurus/plugin-content-docs/client'; +import Translate, { translate } from '@docusaurus/Translate'; +import Layout from '@theme/Layout'; +import styles from './styles.module.css'; +import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter'; +// Very simple pluralization: probably good enough for now +function useDocumentsFoundPlural() { + const { selectMessage } = usePluralForm(); + return (count) => selectMessage(count, translate({ + id: 'theme.SearchPage.documentsFound.plurals', + description: 'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: 'One document found|{count} documents found', + }, { count })); +} +function useDocsSearchVersionsHelpers() { + const allDocsData = useAllDocsData(); + // State of the version select menus / algolia facet filters + // docsPluginId -> versionName map + const [searchVersions, setSearchVersions] = useState(() => Object.entries(allDocsData).reduce((acc, [pluginId, pluginData]) => ({ + ...acc, + [pluginId]: pluginData.versions[0].name, + }), {})); + // Set the value of a single select menu + const setSearchVersion = (pluginId, searchVersion) => setSearchVersions((s) => ({ ...s, [pluginId]: searchVersion })); + const versioningEnabled = Object.values(allDocsData).some((docsData) => docsData.versions.length > 1); + return { + allDocsData, + versioningEnabled, + searchVersions, + setSearchVersion, + }; +} +// We want to display one select per versioned docs plugin instance +function SearchVersionSelectList({ docsSearchVersionsHelpers, }) { + const versionedPluginEntries = Object.entries(docsSearchVersionsHelpers.allDocsData) + // Do not show a version select for unversioned docs plugin instances + .filter(([, docsData]) => docsData.versions.length > 1); + return (
+ {versionedPluginEntries.map(([pluginId, docsData]) => { + const labelPrefix = versionedPluginEntries.length > 1 ? `${pluginId}: ` : ''; + return (); + })} +
); +} +function SearchPageContent() { + const { siteConfig: { themeConfig }, i18n: { currentLocale }, } = useDocusaurusContext(); + const { typesense: { typesenseCollectionName, typesenseServerConfig, typesenseSearchParameters, contextualSearch, externalUrlRegex, }, } = themeConfig; + const documentsFoundPlural = useDocumentsFoundPlural(); + const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers(); + const { searchQuery, setSearchQuery } = useSearchPage(); + // inputValue tracks the live input; searchQuery only updates on submit + const [inputValue, setInputValue] = useState(searchQuery); + // Sync inputValue when searchQuery is populated from the URL on hydration + useEffect(() => { + setInputValue(searchQuery); + }, [searchQuery]); + const initialSearchResultState = { + items: [], + query: null, + totalResults: null, + totalPages: null, + lastPage: null, + hasMore: null, + loading: null, + }; + const [searchResultState, searchResultStateDispatcher] = useReducer((prevState, data) => { + switch (data.type) { + case 'reset': { + return initialSearchResultState; + } + case 'loading': { + return { ...prevState, loading: true }; + } + case 'update': { + if (searchQuery !== data.value.query) { + return prevState; + } + return { + ...data.value, + items: data.value.lastPage === 0 + ? data.value.items + : prevState.items.concat(data.value.items), + }; + } + case 'advance': { + const hasMore = prevState.totalPages > prevState.lastPage + 1; + return { + ...prevState, + lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage, + hasMore, + }; + } + default: + return prevState; + } + }, initialSearchResultState); + // Memoize the adapter and helper so they're only created once, not on every render. + // Creating a new TypesenseInstantSearchAdapter on every render causes repeated + // network activity and accumulates stale event listeners. + // eslint-disable-next-line react-hooks/exhaustive-deps + const typesenseInstantSearchAdapter = useMemo(() => new TypesenseInstantSearchAdapter({ + server: typesenseServerConfig, + additionalSearchParameters: { + // Defaults matching typesense-docsearch-react (SearchBar) behaviour + query_by: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content', + include_fields: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content,anchor,url,type,id', + highlight_full_fields: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content', + group_by: 'url', + group_limit: 1, + sort_by: 'item_priority:desc', + snippet_threshold: 8, + highlight_affix_num_tokens: 4, + ...typesenseSearchParameters, + }, + }), []); // eslint-disable-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps + const algoliaHelper = useMemo(() => algoliaSearchHelper( + typesenseInstantSearchAdapter.searchClient, + typesenseCollectionName, + { + hitsPerPage: typesenseSearchParameters.per_page ?? 20, + advancedSyntax: true, + ...(contextualSearch && { disjunctiveFacets: ['language', 'docusaurus_tag'] }), + } + ), []); // eslint-disable-line react-hooks/exhaustive-deps + useEffect(() => { + const sanitizeValue = (value) => value.replace(/algolia-docsearch-suggestion--highlight/g, 'search-result-match'); + function handleResult({ results: { query, hits, page, nbHits, nbPages } }) { + if (query === '' || !Array.isArray(hits)) { + searchResultStateDispatcher({ type: 'reset' }); + return; + } + const items = hits.map(({ url, _highlightResult, _snippetResult: snippet = {}, }) => { + const parsedURL = new URL(url); + const titles = [0, 1, 2, 3, 4, 5, 6] + .map((lvl) => { + const highlightResult = _highlightResult[`hierarchy.lvl${lvl}`]; + return highlightResult + ? sanitizeValue(highlightResult.value) + : ''; + }) + .filter((v) => v); + return { + title: titles.pop(), + url: isRegexpStringMatch(externalUrlRegex, parsedURL.href) + ? parsedURL.href + : parsedURL.pathname + parsedURL.hash, + summary: snippet.content + ? `${sanitizeValue(snippet.content.value)}...` + : '', + breadcrumbs: titles, + }; + }); + searchResultStateDispatcher({ + type: 'update', + value: { + items, + query, + totalResults: nbHits, + totalPages: nbPages, + lastPage: page, + hasMore: nbPages > page + 1, + loading: false, + }, + }); + } + function handleError(e) { + console.error(e); + } + algoliaHelper.on('result', handleResult); + algoliaHelper.on('error', handleError); + return () => { + algoliaHelper.removeAllListeners('result'); + algoliaHelper.removeAllListeners('error'); + }; + }, [algoliaHelper]); // algoliaHelper is stable (useMemo with []), so this runs once + const [loaderRef, setLoaderRef] = useState(null); + const prevY = useRef(0); + const observer = useRef(ExecutionEnvironment.canUseIntersectionObserver && + new IntersectionObserver((entries) => { + const { isIntersecting, boundingClientRect: { y: currentY }, } = entries[0]; + if (isIntersecting && prevY.current > currentY) { + searchResultStateDispatcher({ type: 'advance' }); + } + prevY.current = currentY; + }, { threshold: 1 })); + const getTitle = () => searchQuery + ? translate({ + id: 'theme.SearchPage.existingResultsTitle', + message: 'Search results for "{query}"', + description: 'The search page title for non-empty query', + }, { + query: searchQuery, + }) + : translate({ + id: 'theme.SearchPage.emptyResultsTitle', + message: 'Search the documentation', + description: 'The search page title for empty query', + }); + const makeSearch = useEvent((page = 0) => { + if (contextualSearch) { + algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default'); + algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale); + Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(([pluginId, searchVersion]) => { + algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', `docs-${pluginId}-${searchVersion}`); + }); + } + algoliaHelper.setQuery(searchQuery).setPage(page).search(); + }); + useEffect(() => { + if (!loaderRef) { + return undefined; + } + const currentObserver = observer.current; + if (currentObserver) { + currentObserver.observe(loaderRef); + return () => currentObserver.unobserve(loaderRef); + } + return () => true; + }, [loaderRef]); + useEffect(() => { + searchResultStateDispatcher({ type: 'reset' }); + if (searchQuery) { + searchResultStateDispatcher({ type: 'loading' }); + makeSearch(); + } + }, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch]); + useEffect(() => { + if (!searchResultState.lastPage || searchResultState.lastPage === 0) { + return; + } + makeSearch(searchResultState.lastPage); + }, [makeSearch, searchResultState.lastPage]); + return ( + + {useTitleFormatter(getTitle())} + {/* + We should not index search pages + See https://github.com/facebook/docusaurus/pull/3233 + */} + + + +
+

{getTitle()}

+ +
{ e.preventDefault(); setSearchQuery(inputValue); }}> +
+
+ setInputValue(e.target.value)} value={inputValue} autoComplete="off" autoFocus/> + +
+
+
+ +
+
+ {!!searchResultState.totalResults && + documentsFoundPlural(searchResultState.totalResults)} +
+ +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + {searchResultState.items.length > 0 ? (
+ {searchResultState.items.map(({ title, url, summary, breadcrumbs }, i) => (
+

+ +

+ + {breadcrumbs.length > 0 && ()} + + {summary && (

)} +

))} +
) : ([ + searchQuery && !searchResultState.loading && (

+ + No results were found + +

), + !!searchResultState.loading && (
), + ])} + + {searchResultState.hasMore && (
+ + Fetching new results... + +
)} +
+ ); +} +export default function SearchPage() { + return ( + + ); +} diff --git a/src/theme/SearchPage/styles.module.css b/src/theme/SearchPage/styles.module.css new file mode 100644 index 000000000..db0175bdc --- /dev/null +++ b/src/theme/SearchPage/styles.module.css @@ -0,0 +1,132 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +.searchQueryInput, +.searchVersionInput { + border-radius: var(--ifm-global-radius); + border: 2px solid var(--ifm-toc-border-color); + font: var(--ifm-font-size-base) var(--ifm-font-family-base); + padding: 0.8rem; + width: 100%; + background: var(--docsearch-searchbox-focus-background); + color: var(--docsearch-text-color); + margin-bottom: 0.5rem; + transition: border var(--ifm-transition-fast) ease; +} + +.searchQueryInput:focus, +.searchVersionInput:focus { + border-color: var(--docsearch-primary-color); + outline: none; +} + +.searchQueryInput::placeholder { + color: var(--docsearch-muted-color); +} + +.searchInputRow { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.searchInputRow .searchQueryInput { + flex: 1; + margin-bottom: 0; +} + +.searchQueryButton { + padding: 0.8rem 1.6rem; + border-radius: var(--ifm-global-radius); + border: 2px solid var(--ifm-color-primary); + background: var(--ifm-color-primary); + color: var(--ifm-color-white); + font: var(--ifm-font-size-base) var(--ifm-font-family-base); + cursor: pointer; + white-space: nowrap; + transition: background var(--ifm-transition-fast) ease, border-color var(--ifm-transition-fast) ease; +} + +.searchQueryButton:hover { + background: var(--ifm-color-primary-dark); + border-color: var(--ifm-color-primary-dark); +} + +.searchResultsColumn { + font-size: 0.9rem; + font-weight: bold; +} + +.algoliaLogo { + max-width: 150px; +} + +.algoliaLogoPathFill { + fill: var(--ifm-font-color-base); +} + +.searchResultItem { + padding: 1rem 0; + border-bottom: 1px solid var(--ifm-toc-border-color); +} + +.searchResultItemHeading { + font-weight: 400; + margin-bottom: 0; +} + +.searchResultItemPath { + font-size: 0.8rem; + color: var(--ifm-color-content-secondary); + --ifm-breadcrumb-separator-size-multiplier: 1; +} + +.searchResultItemSummary { + margin: 0.5rem 0 0; + font-style: italic; +} + +@media only screen and (max-width: 996px) { + .searchResultsColumn { + max-width: 60% !important; + } + + .searchLogoColumn { + max-width: 40% !important; + padding-left: 0 !important; + } +} + +.loadingSpinner { + width: 3rem; + height: 3rem; + border: 0.4em solid #eee; + border-top-color: var(--ifm-color-primary); + border-radius: 50%; + animation: loading-spin 1s linear infinite; + margin: 0 auto; +} + +@keyframes loading-spin { + 100% { + transform: rotate(360deg); + } +} + +.loader { + margin-top: 2rem; +} + +:global(.search-page-wrapper .theme-layout-main) { + width: 100%; +} + +:global(.search-result-match) { + color: var(--docsearch-hit-color); + background: rgb(255 215 142 / 25%); + padding: 0.09em 0; +} diff --git a/src/theme/SearchPage/useSearchPage.js b/src/theme/SearchPage/useSearchPage.js new file mode 100644 index 000000000..abccd751d --- /dev/null +++ b/src/theme/SearchPage/useSearchPage.js @@ -0,0 +1,51 @@ +"use strict"; +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ +Object.defineProperty(exports, "__esModule", { value: true }); +exports.useSearchPage = useSearchPage; +const tslib_1 = require("tslib"); +// Source: https://github.com/facebook/docusaurus/blob/a308fb7c81832cca354192fe2984f52749441249/packages/docusaurus-theme-common/src/hooks/useSearchPage.ts +// Context: https://github.com/typesense/docusaurus-theme-search-typesense/issues/27#issuecomment-1415757477 +const react_1 = require("react"); +const router_1 = require("@docusaurus/router"); +const useDocusaurusContext_1 = tslib_1.__importDefault(require("@docusaurus/useDocusaurusContext")); +const SEARCH_PARAM_QUERY = 'q'; +/** Some utility functions around search queries. */ +function useSearchPage() { + const history = (0, router_1.useHistory)(); + const { siteConfig: { baseUrl }, } = (0, useDocusaurusContext_1.default)(); + const [searchQuery, setSearchQueryState] = (0, react_1.useState)(''); + // Init search query just after React hydration + (0, react_1.useEffect)(() => { + const searchQueryStringValue = + // @ts-ignore + new URLSearchParams(window.location.search).get(SEARCH_PARAM_QUERY) ?? ''; + setSearchQueryState(searchQueryStringValue); + }, []); + const setSearchQuery = (0, react_1.useCallback)((newSearchQuery) => { + // @ts-ignore + const searchParams = new URLSearchParams(window.location.search); + if (newSearchQuery) { + searchParams.set(SEARCH_PARAM_QUERY, newSearchQuery); + } + else { + searchParams.delete(SEARCH_PARAM_QUERY); + } + history.replace({ + search: searchParams.toString(), + }); + setSearchQueryState(newSearchQuery); + }, [history]); + const generateSearchPageLink = (0, react_1.useCallback)((targetSearchQuery) => + // Refer to https://github.com/facebook/docusaurus/pull/2838 + `${baseUrl}search?${SEARCH_PARAM_QUERY}=${encodeURIComponent(targetSearchQuery)}`, [baseUrl]); + return { + searchQuery, + setSearchQuery, + generateSearchPageLink, + }; +} From 6756a4a78b4fed4a205c42b8365ed47f13a6fb8c Mon Sep 17 00:00:00 2001 From: Christopher Hakkaart Date: Fri, 27 Feb 2026 12:25:42 +1300 Subject: [PATCH 2/8] Search page update --- docusaurus.config.js | 11 +- src/theme/SearchPage/index.js | 334 ++++++++++++++++++++++--- src/theme/SearchPage/styles.module.css | 225 +++++++++++++++-- 3 files changed, 521 insertions(+), 49 deletions(-) diff --git a/docusaurus.config.js b/docusaurus.config.js index d1eacf858..a274ceff4 100644 --- a/docusaurus.config.js +++ b/docusaurus.config.js @@ -1,4 +1,5 @@ const path = require("path"); +const platformEnterpriseVersions = require("./platform-enterprise_versions.json"); import "dotenv/config"; import platform_enterprise_latest_version from "./platform-enterprise_latest_version.js"; import { @@ -7,6 +8,14 @@ import { getSeqeraPresetOptions } from "@seqera/docusaurus-preset-seqera"; +// Build the search filter_by dynamically so old platform-enterprise versions are +// excluded automatically whenever a new version is added to versions.json. +// versions.json is ordered newest-first; index 0 is the current/latest version. +const oldEnterpriseVersionTags = platformEnterpriseVersions + .slice(1) + .map((v) => `docs-platform-enterprise-${v}`); +const searchFilterBy = `docusaurus_tag:!=[default,doc_tag_doc_list,blog_posts_list,blog_tags_posts,doc_tags_list,blog_tags_list${oldEnterpriseVersionTags.length ? `,${oldEnterpriseVersionTags.join(",")}` : ""}]`; + export default async function createConfigAsync() { const changelog = { @@ -304,7 +313,7 @@ export default async function createConfigAsync() { per_page: 20, num_typos: 2, prioritize_exact_match: true, - filter_by: 'docusaurus_tag:!=[default,doc_tag_doc_list,blog_posts_list,blog_tags_posts,doc_tags_list,blog_tags_list]', // TODO Remove once the scraper is updated + filter_by: searchFilterBy, // Old platform-enterprise versions excluded automatically via searchFilterBy above }, contextualSearch: false, placeholder: 'Search Seqera docs...', diff --git a/src/theme/SearchPage/index.js b/src/theme/SearchPage/index.js index ea291f043..7084115c5 100644 --- a/src/theme/SearchPage/index.js +++ b/src/theme/SearchPage/index.js @@ -26,6 +26,178 @@ import Translate, { translate } from '@docusaurus/Translate'; import Layout from '@theme/Layout'; import styles from './styles.module.css'; import TypesenseInstantSearchAdapter from 'typesense-instantsearch-adapter'; +// Non-content docusaurus_tag values to always exclude from search results. +// Verified against live Typesense facets — blog/doc-list tags have zero documents +// and are kept here defensively in case content is re-indexed with those tags. +const NON_CONTENT_TAGS = ['default', 'doc_tag_doc_list', 'blog_posts_list', 'blog_tags_posts', 'doc_tags_list', 'blog_tags_list']; +// Maps URL path prefixes to [label, pluginId, customTag]. +// - pluginId: matches the Docusaurus plugin id in allDocsData (versioned/unversioned plugins) +// - customTag: explicit docusaurus_tag for products served via URL rewrite with no local plugin +// (e.g. Nextflow is an external site rewritten to /nextflow/, indexed as docs-default-current) +const PRODUCT_ROUTES = [ + ['/platform-enterprise/', 'Platform Enterprise', 'platform-enterprise', null], + ['/platform-cloud/', 'Platform Cloud', 'platform-cloud', null], + ['/platform-cli/', 'Platform CLI', 'platform-cli', null], + ['/platform-api/', 'Platform API', 'platform-api', null], + ['/nextflow/', 'Nextflow', null, 'docs-default-current'], + ['/multiqc/', 'MultiQC', 'multiqc', null], + ['/wave/', 'Wave', 'wave', null], + ['/fusion/', 'Fusion', 'fusion', null], + ['/changelog/', 'Changelog', null, null], +]; +function getProductLabel(pathname) { + const match = PRODUCT_ROUTES.find(([prefix]) => pathname.startsWith(prefix)); + return match ? match[1] : null; +} +// Custom dropdown for product/version filtering. +// A native always closes and commits a value on click, making it impossible -// to let Platform Enterprise "expand" sub-options without immediately selecting it. -function FilterSelect({ value, onChange, options }) { - const [isOpen, setIsOpen] = useState(false); - const [expandedId, setExpandedId] = useState(null); - const containerRef = useRef(null); - // Close when clicking outside - useEffect(() => { - if (!isOpen) return undefined; - function handleClickOutside(e) { - if (containerRef.current && !containerRef.current.contains(e.target)) { - setIsOpen(false); - } - } - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, [isOpen]); - // Auto-expand the selected product's group when the dropdown opens - useEffect(() => { - if (!isOpen || !value) return; - const productId = value.includes('@') ? value.split('@')[0] : value; - const product = options.find((o) => o.id === productId); - if (product && product.versions.length > 1) { - setExpandedId(productId); - } - }, [isOpen]); // eslint-disable-line react-hooks/exhaustive-deps - function handleSelect(newValue) { - onChange(newValue); - setIsOpen(false); - setExpandedId(null); - } - // Build the trigger label from the current value - const triggerLabel = (() => { - if (!value) return translate({ id: 'theme.SearchPage.allProductsOption', message: 'All products' }); - const [productId, versionName] = value.includes('@') ? value.split('@') : [value, null]; - const product = options.find((o) => o.id === productId); - if (!product) return translate({ id: 'theme.SearchPage.allProductsOption', message: 'All products' }); - if (versionName) { - const version = product.versions.find((v) => v.name === versionName); - return `${product.label} \u2013 ${version?.label || versionName}`; - } - if (product.versions.length > 1) { - const current = product.versions.find((v) => v.isLast) || product.versions[0]; - return `${product.label} \u2013 Current (${current.label})`; - } - return product.label; - })(); - return ( -
-
- -
- {isOpen && ( -
    -
  • handleSelect('')} - > - {translate({ id: 'theme.SearchPage.allProductsOption', message: 'All products' })} -
  • - {options.map((option) => { - if (option.versions.length > 1) { - const isExpanded = expandedId === option.id; - const isActive = value === option.id || value.startsWith(`${option.id}@`); - const currentVersion = option.versions.find((v) => v.isLast) || option.versions[0]; - const olderVersions = option.versions.filter((v) => !v.isLast); - return ( - -
  • setExpandedId(isExpanded ? null : option.id)} - > - {option.label} - -
  • - {isExpanded && ( - <> -
  • handleSelect(option.id)} - > - Current ({currentVersion.label}) -
  • - {olderVersions.map((v, i) => ( -
  • handleSelect(`${option.id}@${v.name}`)} - > - {v.label} -
  • - ))} - - )} -
    - ); - } - return ( -
  • handleSelect(option.id)} - > - {option.label} -
  • - ); - })} -
- )} -
- ); -} -// Very simple pluralization: probably good enough for now -function useDocumentsFoundPlural() { - const { selectMessage } = usePluralForm(); - return (count) => selectMessage(count, translate({ - id: 'theme.SearchPage.documentsFound.plurals', - description: 'Pluralized label for "{count} documents found". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', - message: 'One document found|{count} documents found', - }, { count })); -} -function useDocsSearchVersionsHelpers() { - const allDocsData = useAllDocsData(); - // State of the version select menus / algolia facet filters - // docsPluginId -> versionName map - const [searchVersions, setSearchVersions] = useState(() => Object.entries(allDocsData).reduce((acc, [pluginId, pluginData]) => ({ - ...acc, - [pluginId]: pluginData.versions[0].name, - }), {})); - // Set the value of a single select menu - const setSearchVersion = (pluginId, searchVersion) => setSearchVersions((s) => ({ ...s, [pluginId]: searchVersion })); - const versioningEnabled = Object.values(allDocsData).some((docsData) => docsData.versions.length > 1); - return { - allDocsData, - versioningEnabled, - searchVersions, - setSearchVersion, - }; -} -// We want to display one select per versioned docs plugin instance -function SearchVersionSelectList({ docsSearchVersionsHelpers, }) { - const versionedPluginEntries = Object.entries(docsSearchVersionsHelpers.allDocsData) - // Do not show a version select for unversioned docs plugin instances - .filter(([, docsData]) => docsData.versions.length > 1); - return (
- {versionedPluginEntries.map(([pluginId, docsData]) => { - const labelPrefix = versionedPluginEntries.length > 1 ? `${pluginId}: ` : ''; - return (); - })} -
); -} -function SearchPageContent() { - const { siteConfig: { themeConfig }, i18n: { currentLocale }, } = useDocusaurusContext(); - const { typesense: { typesenseCollectionName, typesenseServerConfig, typesenseSearchParameters, contextualSearch, externalUrlRegex, }, } = themeConfig; - const documentsFoundPlural = useDocumentsFoundPlural(); - const docsSearchVersionsHelpers = useDocsSearchVersionsHelpers(); - // Compute tags for old versions of versioned plugins to exclude from results, - // so only the latest version of each plugin appears by default. - // The exclusion approach (rather than a whitelist) is used deliberately so that - // content accessible via URL rewrites or non-standard tags is not accidentally dropped. - const oldVersionTags = useMemo(() => { - const tags = []; - Object.entries(docsSearchVersionsHelpers.allDocsData).forEach(([pluginId, pluginData]) => { - if (pluginData.versions.length > 1) { - const latest = pluginData.versions.find((v) => v.isLast) || pluginData.versions[0]; - pluginData.versions - .filter((v) => v.name !== latest.name) - .forEach((v) => tags.push(`docs-${pluginId}-${v.name}`)); - } - }); - return tags; - }, []); // eslint-disable-line react-hooks/exhaustive-deps - const { searchQuery, setSearchQuery } = useSearchPage(); - // inputValue tracks the live input; searchQuery only updates on submit - const [inputValue, setInputValue] = useState(searchQuery); - // Sync inputValue when searchQuery is populated from the URL on hydration - useEffect(() => { - setInputValue(searchQuery); - }, [searchQuery]); - // Single filter state: '' | 'productId' | 'productId@versionName' - const [selectedFilter, setSelectedFilter] = useState(''); - // Products available for filtering — plugin-based products (from allDocsData) and - // rewrite-based products with a known customTag (e.g. Nextflow → docs-default-current). - // Each entry has a stable `id` used as the select option value: - // - plugin-based: id = pluginId - // - rewrite-based: id = customTag (pluginId is null) - const productOptions = useMemo(() => PRODUCT_ROUTES - .filter(([,, pluginId, customTag]) => - (pluginId && docsSearchVersionsHelpers.allDocsData[pluginId]) || customTag - ) - .map(([, label, pluginId, customTag]) => ({ - id: pluginId || customTag, - label, - pluginId, - customTag, - versions: pluginId && docsSearchVersionsHelpers.allDocsData[pluginId] - ? docsSearchVersionsHelpers.allDocsData[pluginId].versions - : [], - })), - []); // eslint-disable-line react-hooks/exhaustive-deps - const initialSearchResultState = { - items: [], - query: null, - totalResults: null, - totalPages: null, - lastPage: null, - hasMore: null, - loading: null, - }; - const [searchResultState, searchResultStateDispatcher] = useReducer((prevState, data) => { - switch (data.type) { - case 'reset': { - return initialSearchResultState; - } - case 'loading': { - return { ...prevState, loading: true }; - } - case 'update': { - if (searchQuery !== data.value.query) { - return prevState; - } - return { - ...data.value, - items: data.value.lastPage === 0 - ? data.value.items - : prevState.items.concat(data.value.items), - }; - } - case 'advance': { - const hasMore = prevState.totalPages > prevState.lastPage + 1; - return { - ...prevState, - lastPage: hasMore ? prevState.lastPage + 1 : prevState.lastPage, - hasMore, - }; - } - default: - return prevState; - } - }, initialSearchResultState); - // Memoize the adapter and helper so they're only created once, not on every render. - // Creating a new TypesenseInstantSearchAdapter on every render causes repeated - // network activity and accumulates stale event listeners. - // eslint-disable-next-line react-hooks/exhaustive-deps - const typesenseInstantSearchAdapter = useMemo(() => { - // Parse selectedFilter: '' | 'productId' | 'productId@versionName' - const atIdx = selectedFilter.indexOf('@'); - const filterId = atIdx >= 0 ? selectedFilter.slice(0, atIdx) : selectedFilter; - const filterVersion = atIdx >= 0 ? selectedFilter.slice(atIdx + 1) : null; - const filterProduct = filterId ? productOptions.find((p) => p.id === filterId) : null; - let filterBy; - if (filterProduct) { - // Specific product selected: use an explicit inclusion filter so old versions - // are not accidentally blocked by the config-level exclusion list. - // We rebuild from NON_CONTENT_TAGS rather than typesenseSearchParameters.filter_by - // because the config filter already excludes old version tags, which would - // conflict when the user intentionally selects an older version. - let inclusionTag; - if (filterProduct.customTag) { - // Rewrite-based product (e.g. Nextflow): use its known docusaurus_tag directly - inclusionTag = filterProduct.customTag; - } else { - const targetVersionName = filterVersion - || (filterProduct.versions.find((v) => v.isLast) || filterProduct.versions[0]).name; - inclusionTag = `docs-${filterProduct.pluginId}-${targetVersionName}`; - } - filterBy = [ - `docusaurus_tag:!=[${NON_CONTENT_TAGS.join(',')}]`, - `docusaurus_tag:=[${inclusionTag}]`, - ].join(' && '); - } else { - // All products: use config filter + exclude remaining old version tags. - // Exclusion (rather than a whitelist) ensures content accessible via URL - // rewrites or non-standard tags is not accidentally dropped. - const versionExclusion = oldVersionTags.length > 0 - ? `docusaurus_tag:!=[${oldVersionTags.join(',')}]` - : null; - filterBy = [typesenseSearchParameters.filter_by, versionExclusion] - .filter(Boolean) - .join(' && '); - } - return new TypesenseInstantSearchAdapter({ - server: typesenseServerConfig, - additionalSearchParameters: { - // Defaults matching typesense-docsearch-react (SearchBar) behaviour - query_by: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content', - include_fields: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content,anchor,url,type,id', - highlight_full_fields: 'hierarchy.lvl0,hierarchy.lvl1,hierarchy.lvl2,hierarchy.lvl3,hierarchy.lvl4,hierarchy.lvl5,hierarchy.lvl6,content', - group_by: 'url', - group_limit: 1, - sort_by: 'item_priority:desc', - snippet_threshold: 8, - highlight_affix_num_tokens: 4, - ...typesenseSearchParameters, - filter_by: filterBy, - }, - }); - }, [selectedFilter]); // eslint-disable-line react-hooks/exhaustive-deps - // eslint-disable-next-line react-hooks/exhaustive-deps - const algoliaHelper = useMemo(() => algoliaSearchHelper( - typesenseInstantSearchAdapter.searchClient, - typesenseCollectionName, - { - hitsPerPage: typesenseSearchParameters.per_page ?? 20, - advancedSyntax: true, - ...(contextualSearch && { disjunctiveFacets: ['language', 'docusaurus_tag'] }), - } - ), [typesenseInstantSearchAdapter]); // eslint-disable-line react-hooks/exhaustive-deps - useEffect(() => { - const sanitizeValue = (value) => value.replace(/algolia-docsearch-suggestion--highlight/g, 'search-result-match'); - function handleResult({ results: { query, hits, page, nbHits, nbPages } }) { - if (query === '' || !Array.isArray(hits)) { - searchResultStateDispatcher({ type: 'reset' }); - return; - } - const items = hits.map((hit) => { - const { url, _highlightResult, _snippetResult: snippet = {} } = hit; - const parsedURL = new URL(url); - // Build levels using both raw and highlighted values. - // Raw values are plain text matching what the page breadcrumbs show. - // Highlighted values show which part of the hierarchy matched the query. - const levels = [0, 1, 2, 3, 4, 5, 6] - .map((lvl) => { - // Raw value: try dot-notation key first, then nested object - const raw = hit[`hierarchy.lvl${lvl}`] - || hit.hierarchy?.[`lvl${lvl}`] - || ''; - const h = _highlightResult[`hierarchy.lvl${lvl}`]; - const highlighted = h ? sanitizeValue(h.value) : raw; - return { raw, highlighted }; - }) - .filter((l) => l.raw); - // Last level is the page/section title; remainder are breadcrumbs - const titleLevel = levels.pop(); - const product = getProductLabel(parsedURL.pathname); - // Replace lvl0 ("Documentation") with the product label - if (product && levels.length > 0) { - levels[0] = { raw: product, highlighted: product }; - } else if (product) { - levels.unshift({ raw: product, highlighted: product }); - } - const resultUrl = isRegexpStringMatch(externalUrlRegex, parsedURL.href) - ? parsedURL.href - : parsedURL.pathname + parsedURL.hash; - return { - title: titleLevel?.highlighted || '', - url: resultUrl, - summary: snippet.content - ? `${sanitizeValue(snippet.content.value)}...` - : '', - // Include all levels (parent categories + current page/section) - // so the breadcrumb matches the full path shown on the page. - breadcrumbs: [...levels, ...(titleLevel ? [titleLevel] : [])].map((l) => l.raw), - }; - }); - searchResultStateDispatcher({ - type: 'update', - value: { - items, - query, - totalResults: nbHits, - totalPages: nbPages, - lastPage: page, - hasMore: nbPages > page + 1, - loading: false, - }, - }); - } - function handleError(e) { - console.error(e); - } - algoliaHelper.on('result', handleResult); - algoliaHelper.on('error', handleError); - return () => { - algoliaHelper.removeAllListeners('result'); - algoliaHelper.removeAllListeners('error'); - }; - }, [algoliaHelper]); // algoliaHelper is stable (useMemo with []), so this runs once - const [loaderRef, setLoaderRef] = useState(null); - const prevY = useRef(0); - const observer = useRef(ExecutionEnvironment.canUseIntersectionObserver && - new IntersectionObserver((entries) => { - const { isIntersecting, boundingClientRect: { y: currentY }, } = entries[0]; - if (isIntersecting && prevY.current > currentY) { - searchResultStateDispatcher({ type: 'advance' }); - } - prevY.current = currentY; - }, { threshold: 1 })); - const getTitle = () => searchQuery - ? translate({ - id: 'theme.SearchPage.existingResultsTitle', - message: 'Search results for "{query}"', - description: 'The search page title for non-empty query', - }, { - query: searchQuery, - }) - : translate({ - id: 'theme.SearchPage.emptyResultsTitle', - message: 'Search the documentation', - description: 'The search page title for empty query', - }); - const makeSearch = useEvent((page = 0) => { - if (contextualSearch) { - algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', 'default'); - algoliaHelper.addDisjunctiveFacetRefinement('language', currentLocale); - Object.entries(docsSearchVersionsHelpers.searchVersions).forEach(([pluginId, searchVersion]) => { - algoliaHelper.addDisjunctiveFacetRefinement('docusaurus_tag', `docs-${pluginId}-${searchVersion}`); - }); - } - algoliaHelper.setQuery(searchQuery).setPage(page).search(); - }); - useEffect(() => { - if (!loaderRef) { - return undefined; - } - const currentObserver = observer.current; - if (currentObserver) { - currentObserver.observe(loaderRef); - return () => currentObserver.unobserve(loaderRef); - } - return () => true; - }, [loaderRef]); - useEffect(() => { - searchResultStateDispatcher({ type: 'reset' }); - if (searchQuery) { - searchResultStateDispatcher({ type: 'loading' }); - makeSearch(); - } - }, [searchQuery, docsSearchVersionsHelpers.searchVersions, makeSearch, selectedFilter]); - useEffect(() => { - if (!searchResultState.lastPage || searchResultState.lastPage === 0) { - return; - } - makeSearch(searchResultState.lastPage); - }, [makeSearch, searchResultState.lastPage]); - return ( - - {useTitleFormatter(getTitle())} - {/* - We should not index search pages - See https://github.com/facebook/docusaurus/pull/3233 - */} - - - -
-

{getTitle()}

- -
{ e.preventDefault(); setSearchQuery(inputValue); }}> -
-
- setInputValue(e.target.value)} value={inputValue} autoComplete="off" autoFocus/> - {productOptions.length > 0 && ( - - )} - -
-
-
- -
-
- {!!searchResultState.totalResults && - documentsFoundPlural(searchResultState.totalResults)} -
- - -
- - {searchResultState.items.length > 0 ? (
- {searchResultState.items.map(({ title, url, summary, breadcrumbs }, i) => (
-

- -

- - {breadcrumbs.length > 0 && ()} - - {summary && (

)} -

))} -
) : ([ - searchQuery && !searchResultState.loading && (

- - No results were found - -

), - !!searchResultState.loading && (
), - ])} - - {searchResultState.hasMore && (
- - Fetching new results... - -
)} -
- ); -} -export default function SearchPage() { - return ( - - ); -} diff --git a/src/theme/SearchPage/styles.module.css b/src/theme/SearchPage/styles.module.css deleted file mode 100644 index cc5fb932c..000000000 --- a/src/theme/SearchPage/styles.module.css +++ /dev/null @@ -1,325 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -.searchQueryInput, -.searchVersionInput { - border-radius: 0.5rem; - border: 1px solid #d1d5db; - font-size: 0.875rem; - height: 2.25rem; - padding: 0 0.75rem; - width: 100%; - background: white; - color: #111827; - margin-bottom: 0.5rem; - transition: border-color 0.15s; -} - -.searchQueryInput:focus, -.searchVersionInput:focus { - border-color: #6b7280; - outline: none; -} - -.searchQueryInput::placeholder { - color: #6b7280; -} - -[data-theme='dark'] .searchQueryInput, -[data-theme='dark'] .searchVersionInput { - background: #111827; - border-color: #374151; - color: white; -} - -[data-theme='dark'] .searchQueryInput:focus, -[data-theme='dark'] .searchVersionInput:focus { - border-color: #4b5563; -} - -[data-theme='dark'] .searchQueryInput::placeholder { - color: #6b7280; -} - -.searchInputRow { - display: flex; - gap: 0.5rem; - align-items: center; - flex-wrap: wrap; -} - -.searchInputRow .searchQueryInput { - flex: 1; - margin-bottom: 0; -} - -/* At narrow widths the input takes the full first row; - the filter and search button drop together to the next row */ -@media (max-width: 640px) { - .searchInputRow .searchQueryInput { - flex: 0 0 100%; - } -} - -.searchQueryButton { - border-radius: 4px; - transition: all 0.3s; - color: black; - background: var(--color-nextflow-500); - border: 1px solid var(--color-nextflow-700); - font-weight: 500; - font-size: 0.9rem; - text-align: center; - padding: 0.3rem 1rem; - cursor: pointer; - white-space: nowrap; -} - -.searchQueryButton:hover { - background: var(--color-nextflow-700); - border-color: var(--color-nextflow-700); -} - -[data-theme='dark'] .searchQueryButton { - color: black; - background: var(--color-nextflow-400); - border: 1px solid var(--color-nextflow-400); -} - -[data-theme='dark'] .searchQueryButton:hover { - background: var(--color-nextflow-600); - border-color: var(--color-nextflow-700); -} - -.filterSelectWrapper { - position: relative; - width: 16.25rem; -} - -/* Matches VersionSwitcher: rounded-lg border border-gray-300 overflow-hidden bg-white */ -.filterBox { - border-radius: 0.5rem; - border: 1px solid #d1d5db; - overflow: hidden; - background: white; -} - -/* Matches VersionSwitcher: rounded-b-none when open */ -.filterBoxOpen { - border-bottom-left-radius: 0; - border-bottom-right-radius: 0; -} - -/* Matches VersionSwitcher trigger: h-9 px-3 text-sm text-gray-900 bg-transparent hover:bg-gray-100 */ -.filterTrigger { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.5rem; - height: 2.25rem; - width: 100%; - padding: 0 0.75rem; - background: transparent; - border: none; - color: #111827; - font-size: 0.875rem; - text-align: left; - cursor: pointer; - white-space: nowrap; - transition: background 0.15s; -} - -.filterTrigger:hover { - background: #f3f4f6; -} - -.filterTrigger:focus { - outline: none; -} - -.filterTriggerChevron { - display: block; - width: 1.25rem; - height: 1.25rem; - flex-shrink: 0; - transition: transform 0.2s; -} - -.filterTriggerChevronOpen { - transform: rotate(0deg); -} - -.filterTriggerChevronClosed { - transform: rotate(-90deg); -} - -/* Matches VersionSwitcher dropdown: bg-white border-gray-300 border-t-gray-200 rounded-b-lg */ -.filterDropdown { - position: absolute; - top: 100%; - left: 0; - min-width: 100%; - margin: -1px 0 0; - padding: 0; - list-style: none; - background: white; - border: 1px solid #d1d5db; - border-top-color: #e5e7eb; - border-bottom-left-radius: 0.5rem; - border-bottom-right-radius: 0.5rem; - overflow: hidden; - z-index: 50; -} - -/* Matches VersionSwitcher items: h-9 px-3 text-sm text-gray-900 border-t border-gray-200 hover:bg-gray-100 */ -.filterOption { - display: flex; - align-items: center; - justify-content: space-between; - height: 2.25rem; - padding: 0 0.75rem; - font-size: 0.875rem; - color: #111827; - cursor: pointer; - white-space: nowrap; - user-select: none; - border-top: 1px solid #e5e7eb; - transition: background 0.15s; -} - -.filterOption:first-child { - border-top: none; -} - -.filterOption:hover { - background: #f3f4f6; -} - -.filterOptionActive { - color: var(--ifm-color-primary); -} - -.filterExpandChevron { - display: block; - width: 1.25rem; - height: 1.25rem; - margin-left: 0.5rem; - flex-shrink: 0; - transition: transform 0.2s; -} - -.filterSubOption { - padding-left: 1.5rem; -} - -/* Dark mode — matches VersionSwitcher: dark:bg-gray-900 dark:border-gray-700 dark:text-white dark:hover:bg-gray-800 */ -[data-theme='dark'] .filterBox { - background: #111827; - border-color: #374151; -} - -[data-theme='dark'] .filterTrigger { - color: white; -} - -[data-theme='dark'] .filterTrigger:hover { - background: #1f2937; -} - -[data-theme='dark'] .filterDropdown { - background: #111827; - border-color: #374151; - border-top-color: #4b5563; -} - -[data-theme='dark'] .filterOption { - color: white; - border-top-color: #374151; -} - -[data-theme='dark'] .filterOption:hover { - background: #1f2937; -} - -.searchResultsColumn { - font-size: 0.9rem; - font-weight: bold; -} - -.algoliaLogo { - max-width: 150px; -} - -.algoliaLogoPathFill { - fill: var(--ifm-font-color-base); -} - -.searchResultItem { - padding: 1rem 0; - border-bottom: 1px solid var(--ifm-toc-border-color); -} - -.searchResultItemHeading { - font-weight: 400; - margin-bottom: 0; -} - -.searchResultItemPath { - font-size: 0.8rem; - color: var(--ifm-font-color-base); - --ifm-breadcrumb-separator-size-multiplier: 1; -} - -.searchResultItemPath :global(.breadcrumbs__item:not(:last-child))::after { - margin: 0 0.1875rem; -} - -.searchResultItemSummary { - margin: 0.5rem 0 0; - font-style: italic; -} - -@media only screen and (max-width: 996px) { - .searchResultsColumn { - max-width: 60% !important; - } - - .searchLogoColumn { - max-width: 40% !important; - padding-left: 0 !important; - } -} - -.loadingSpinner { - width: 3rem; - height: 3rem; - border: 0.4em solid #eee; - border-top-color: var(--ifm-color-primary); - border-radius: 50%; - animation: loading-spin 1s linear infinite; - margin: 2rem auto 0; -} - -@keyframes loading-spin { - 100% { - transform: rotate(360deg); - } -} - -.loader { - margin-top: 6rem; -} - -:global(.search-page-wrapper .theme-layout-main) { - width: 100%; -} - -:global(.search-result-match) { - color: var(--docsearch-hit-color); - background: rgb(255 215 142 / 25%); - padding: 0.09em 0; -} diff --git a/src/theme/SearchPage/useSearchPage.js b/src/theme/SearchPage/useSearchPage.js deleted file mode 100644 index f5ffe46b5..000000000 --- a/src/theme/SearchPage/useSearchPage.js +++ /dev/null @@ -1,51 +0,0 @@ -"use strict"; -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ -Object.defineProperty(exports, "__esModule", { value: true }); -exports.useSearchPage = useSearchPage; -const tslib_1 = require("tslib"); -// Source: https://github.com/facebook/docusaurus/blob/a308fb7c81832cca354192fe2984f52749441249/packages/docusaurus-theme-common/src/hooks/useSearchPage.ts -// Context: https://github.com/typesense/docusaurus-theme-search-typesense/issues/27#issuecomment-1415757477 -const react_1 = require("react"); -const router_1 = require("@docusaurus/router"); -const useDocusaurusContext_1 = tslib_1.__importDefault(require("@docusaurus/useDocusaurusContext")); -const SEARCH_PARAM_QUERY = 'q'; -/** Some utility functions around search queries. */ -function useSearchPage() { - const history = (0, router_1.useHistory)(); - const { siteConfig: { baseUrl }, } = (0, useDocusaurusContext_1.default)(); - const [searchQuery, setSearchQueryState] = (0, react_1.useState)(''); - // Init search query just after React hydration - (0, react_1.useEffect)(() => { - const searchQueryStringValue = - // @ts-ignore - new URLSearchParams(window.location.search).get(SEARCH_PARAM_QUERY) ?? ''; - setSearchQueryState(searchQueryStringValue); - }, []); - const setSearchQuery = (0, react_1.useCallback)((newSearchQuery) => { - // @ts-ignore - const searchParams = new URLSearchParams(window.location.search); - if (newSearchQuery) { - searchParams.set(SEARCH_PARAM_QUERY, newSearchQuery); - } - else { - searchParams.delete(SEARCH_PARAM_QUERY); - } - history.replace({ - search: searchParams.toString(), - }); - setSearchQueryState(newSearchQuery); - }, [history]); - const generateSearchPageLink = (0, react_1.useCallback)((targetSearchQuery) => - // Refer to https://github.com/facebook/docusaurus/pull/2838 - `${baseUrl}search?${SEARCH_PARAM_QUERY}=${encodeURIComponent(targetSearchQuery)}`, [baseUrl]); - return { - searchQuery, - setSearchQuery, - generateSearchPageLink, - }; -} From ac1f1a72a5bbda35502a232c15a1d8833c5cab67 Mon Sep 17 00:00:00 2001 From: Christopher Hakkaart Date: Mon, 9 Mar 2026 16:35:52 +1300 Subject: [PATCH 8/8] Update version --- package-lock.json | 18 ++++++++++-------- package.json | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2667ba72d..a913ac90a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "dependencies": { "@docusaurus/faster": "^3.9.2", "@rspack/core": "^1.4.11", - "@seqera/docusaurus-preset-seqera": "^1.0.25", + "@seqera/docusaurus-preset-seqera": "^1.0.26", "image-size": "^2.0.2", "postcss-import": "^16.1.1", "postcss-loader": "^8.1.1", @@ -5785,9 +5785,9 @@ "license": "MIT" }, "node_modules/@seqera/docusaurus-preset-seqera": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@seqera/docusaurus-preset-seqera/-/docusaurus-preset-seqera-1.0.25.tgz", - "integrity": "sha512-/pt7JRdP2XBCA0lu6f1eUaAIsEMTJ4T0di9VFs2SxhSx8f+FIaAsOPfaSj51qAOuCDZpNu0JyzmdrvWi5yfEkw==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@seqera/docusaurus-preset-seqera/-/docusaurus-preset-seqera-1.0.26.tgz", + "integrity": "sha512-IpScJZHH3ebYmDKEpOwFN0aRQtdLQKR5TukrGg/X+UIYsrtp/1lJuieGD3MqVtPOFppyGeYhGnMSSBP/J/h9Vw==", "license": "Apache-2.0", "dependencies": { "@docusaurus/core": "3.9.2", @@ -5804,7 +5804,7 @@ "@docusaurus/theme-common": "3.9.2", "@docusaurus/theme-search-algolia": "3.9.2", "@docusaurus/types": "3.9.2", - "@seqera/docusaurus-theme-seqera": "1.0.25", + "@seqera/docusaurus-theme-seqera": "1.0.26", "@tailwindcss/oxide": "^4.1.17", "docusaurus-plugin-llms": "^0.3.0", "docusaurus-plugin-openapi-docs": "^4.5.1", @@ -5825,9 +5825,9 @@ } }, "node_modules/@seqera/docusaurus-theme-seqera": { - "version": "1.0.25", - "resolved": "https://registry.npmjs.org/@seqera/docusaurus-theme-seqera/-/docusaurus-theme-seqera-1.0.25.tgz", - "integrity": "sha512-f5mFYeZfP7br3qbCl8mN3H7nr0sGOUqvBeOo/1OIgBr44jWnGb9s1A0Y8cDVUldHuN8f9WszhedO2zKdCTo2oA==", + "version": "1.0.26", + "resolved": "https://registry.npmjs.org/@seqera/docusaurus-theme-seqera/-/docusaurus-theme-seqera-1.0.26.tgz", + "integrity": "sha512-vxOdXvXTb4x0s8JWvEQN59wpES2tpzFNjaU9P3BK7V3RmKZWvI1PnqotZgJCscKcIhMdVa7BaakAiBxkCNuBiw==", "license": "Apache-2.0", "dependencies": { "@docusaurus/core": "3.9.2", @@ -5845,6 +5845,7 @@ "@docusaurus/utils-validation": "3.9.2", "@mdx-js/react": "^3.0.0", "@tailwindcss/postcss": "^4.1.17", + "algoliasearch-helper": "^3.0.0", "clsx": "^2.0.0", "infima": "0.2.0-alpha.45", "lodash": "^4.17.21", @@ -5857,6 +5858,7 @@ "rtlcss": "^4.1.0", "tailwindcss": "^4.1.17", "tslib": "^2.6.0", + "typesense-instantsearch-adapter": "^2.0.0", "utility-types": "^3.10.0" }, "engines": { diff --git a/package.json b/package.json index 3d2680f4a..ce744fc83 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "dependencies": { "@docusaurus/faster": "^3.9.2", "@rspack/core": "^1.4.11", - "@seqera/docusaurus-preset-seqera": "^1.0.25", + "@seqera/docusaurus-preset-seqera": "^1.0.26", "image-size": "^2.0.2", "postcss-import": "^16.1.1", "postcss-loader": "^8.1.1",