From c490ecea1e21749e7f3a05ecf7682cf0bf36a5dd Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Fri, 23 Oct 2026 11:43:03 +0200 Subject: [PATCH 1/5] Normalize component identifiers in docs search (ENG-13516) --- packages/reflex-site-shared/README.md | 7 ++ .../+normalize-component-search.bugfix.md | 1 + .../assets/components/AlgoliaSearch.tsx | 6 +- .../units/reflex_site_shared/test_algolia.py | 96 +++++++++++++++++++ 4 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 packages/reflex-site-shared/news/+normalize-component-search.bugfix.md diff --git a/packages/reflex-site-shared/README.md b/packages/reflex-site-shared/README.md index d04dd97f0ab..a710653feac 100644 --- a/packages/reflex-site-shared/README.md +++ b/packages/reflex-site-shared/README.md @@ -108,6 +108,13 @@ the exported `render_docgen_document` helper directly. Add `defer` to a `python demo exec` fence to mount a heavy preview as it approaches the viewport. The example's source code remains in the initial HTML; the preview reserves 450px of height and stays mounted once shown. This is useful for pages containing many Plotly charts. +### Component search + +The shared search matches complete `rx.*` identifiers to words in documentation +titles: `rx.button` searches for `button`, `rx.radio_group` for `radio group`, +and `rx.alert_dialog.root` for `alert dialog root`. The input keeps the original +text. Ordinary search phrases are sent unchanged. + ## Marketing footer Use the marketing site's responsive five-column directory footer in another diff --git a/packages/reflex-site-shared/news/+normalize-component-search.bugfix.md b/packages/reflex-site-shared/news/+normalize-component-search.bugfix.md new file mode 100644 index 00000000000..98cde988cdd --- /dev/null +++ b/packages/reflex-site-shared/news/+normalize-component-search.bugfix.md @@ -0,0 +1 @@ +Normalize component searches such as `rx.button` and `rx.radio_group` to match documentation titles, improving component page relevance. diff --git a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx index 77cfe3e4e28..175a7417eff 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx +++ b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx @@ -439,6 +439,10 @@ export function AlgoliaSearch() { requestRef.current = controller; const timer = window.setTimeout(async () => { try { + // Match component identifiers to the words used in page titles. + const searchQuery = /^rx\.[a-z]\w*(?:\.[a-z]\w*)*$/i.test(trimmedQuery) + ? trimmedQuery.slice(3).replace(/[_.]+/g, " ") + : trimmedQuery; const response = await fetch(ALGOLIA_SEARCH_ENDPOINT, { method: "POST", headers: { @@ -447,7 +451,7 @@ export function AlgoliaSearch() { "X-Algolia-Application-Id": ALGOLIA_APP_ID, }, body: JSON.stringify({ - query: trimmedQuery, + query: searchQuery, hitsPerPage: 10, attributesToRetrieve: [ "objectID", diff --git a/tests/units/reflex_site_shared/test_algolia.py b/tests/units/reflex_site_shared/test_algolia.py index 0c21ab5efba..693dc422584 100644 --- a/tests/units/reflex_site_shared/test_algolia.py +++ b/tests/units/reflex_site_shared/test_algolia.py @@ -1,11 +1,107 @@ """Tests for the shared keyword-only Algolia search.""" +import json +import shutil +import subprocess from pathlib import Path +import pytest from reflex_site_shared.components.algolia import AlgoliaSearch, Search from reflex_site_shared.plugins import SharedSiteStylesPlugin +@pytest.mark.skipif(shutil.which("node") is None, reason="node missing") +@pytest.mark.parametrize( + ("query", "expected_query"), + [ + ("rx.button", "button"), + ("rx.input", "input"), + ("rx.radio_group", "radio group"), + ("rx.data_table", "data table"), + ("rx.alert_dialog.root", "alert dialog root"), + ("rx.scroll_area", "scroll area"), + (" rx.button ", "button"), + ("RX.BUTTON", "BUTTON"), + ("button", "button"), + ("radio group", "radio group"), + ("on_click", "on_click"), + ("how to style rx.button", "how to style rx.button"), + ("example.rx.button", "example.rx.button"), + ("https://rx.button", "https://rx.button"), + ("rx.", "rx."), + ], +) +def test_algolia_search_normalizes_component_queries( + query: str, expected_query: str +) -> None: + """Send component names as title words while preserving input and cache keys. + + Args: + query: User-entered search text. + expected_query: Query sent to Algolia. + """ + assets = dict(SharedSiteStylesPlugin().get_static_assets()) + source = assets[Path("public/components/AlgoliaSearch.tsx")] + # Execute the request effect with a fake timer and transport, without React. + effect = source.rsplit("useEffect(() => {", 1)[1].split("}, [isOpen, query]);", 1)[ + 0 + ] + effect = effect.replace(" as AlgoliaResponse", "") + script = """ +const query = JSON.parse(process.argv[1]); +const isOpen = true; +const MIN_QUERY_LENGTH = 2; +const MAX_CACHED_QUERIES = 50; +const SEARCH_DEBOUNCE_MS = 350; +const ALGOLIA_SEARCH_ENDPOINT = "https://search.invalid/query"; +const ALGOLIA_APP_ID = "test"; +const ALGOLIA_SEARCH_API_KEY = "test"; +const cacheRef = { current: new Map() }; +const requestRef = { current: null }; +const state = {}; +const setHits = (value) => { state.hits = value; }; +const setNbHits = (value) => { state.nbHits = value; }; +const setStatus = (value) => { state.status = value; }; +const setResultsQuery = (value) => { state.resultsQuery = value; }; +const setActiveIndex = (value) => { state.activeIndex = value; }; +const normalizeHits = (hits) => hits; +const requests = []; +const fetch = async (url, options) => { + requests.push(JSON.parse(options.body)); + return { ok: true, json: async () => ({ hits: [], nbHits: 0 }) }; +}; +let runTimer; +const window = { setTimeout: (callback) => { runTimer = callback; } }; +const runEffect = () => { +""" + script += effect + script += """ +}; +runEffect(); +await runTimer(); +runEffect(); +console.log(JSON.stringify({ + requests, + query, + cacheKeys: [...cacheRef.current.keys()], + state, +})); +""" + result = subprocess.run( + ["node", "--input-type=module", "--eval", script, json.dumps(query)], + capture_output=True, + encoding="utf-8", + timeout=10, + check=True, + ) + output = json.loads(result.stdout) + assert [request["query"] for request in output["requests"]] == [expected_query] + assert output["query"] == query + assert output["cacheKeys"] == [query.strip().lower()] + assert output["state"]["resultsQuery"] == query.strip().lower() + assert output["state"]["status"] == "ready" + + def test_algolia_search_uses_local_keyword_only_component() -> None: """Use the local Algolia UI without shipping an AI search dependency.""" assert AlgoliaSearch.library == "$/public/components/AlgoliaSearch" From 38b01e48c7d061d82e1b594aa1bc134dd5950792 Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Fri, 23 Oct 2026 11:50:54 +0200 Subject: [PATCH 2/5] Test docs search normalization through a named helper --- .../src/reflex_site_shared/plugins.py | 1 + .../assets/components/AlgoliaSearch.tsx | 8 +- .../styles/assets/components/search-query.mjs | 11 +++ .../units/reflex_site_shared/test_algolia.py | 76 +++++-------------- .../units/reflex_site_shared/test_plugins.py | 1 + 5 files changed, 37 insertions(+), 60 deletions(-) create mode 100644 packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs diff --git a/packages/reflex-site-shared/src/reflex_site_shared/plugins.py b/packages/reflex-site-shared/src/reflex_site_shared/plugins.py index 1ff2dba698c..eb34b0d78a1 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/plugins.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/plugins.py @@ -25,6 +25,7 @@ "icons/search.svg", "components/marketing-date.jsx", "homepage/lib/use-isomorphic-layout-effect.ts", + "components/search-query.mjs", ) diff --git a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx index 175a7417eff..a80284657d2 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx +++ b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx @@ -22,6 +22,8 @@ import React, { } from "react"; import { createPortal } from "react-dom"; +import { normalizeSearchQuery } from "./search-query.mjs"; + const ALGOLIA_APP_ID = "WLK9YABRW4"; const ALGOLIA_SEARCH_API_KEY = "7abb638647d3aa25e7a6d29dfdca2212"; const ALGOLIA_INDEX_NAME = "reflex_dev_wlk9yabrw4_pages"; @@ -439,10 +441,6 @@ export function AlgoliaSearch() { requestRef.current = controller; const timer = window.setTimeout(async () => { try { - // Match component identifiers to the words used in page titles. - const searchQuery = /^rx\.[a-z]\w*(?:\.[a-z]\w*)*$/i.test(trimmedQuery) - ? trimmedQuery.slice(3).replace(/[_.]+/g, " ") - : trimmedQuery; const response = await fetch(ALGOLIA_SEARCH_ENDPOINT, { method: "POST", headers: { @@ -451,7 +449,7 @@ export function AlgoliaSearch() { "X-Algolia-Application-Id": ALGOLIA_APP_ID, }, body: JSON.stringify({ - query: searchQuery, + query: normalizeSearchQuery(trimmedQuery), hitsPerPage: 10, attributesToRetrieve: [ "objectID", diff --git a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs new file mode 100644 index 00000000000..1b967442d7d --- /dev/null +++ b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs @@ -0,0 +1,11 @@ +/** + * Convert a complete Reflex component identifier to searchable title words. + * + * @param {string} query A trimmed search query. + * @returns {string} The component's title words, or the unchanged query. + */ +export function normalizeSearchQuery(query) { + return /^rx\.[a-z]\w*(?:\.[a-z]\w*)*$/i.test(query) + ? query.slice(3).replace(/[_.]+/g, " ") + : query; +} diff --git a/tests/units/reflex_site_shared/test_algolia.py b/tests/units/reflex_site_shared/test_algolia.py index 693dc422584..a74d99d4583 100644 --- a/tests/units/reflex_site_shared/test_algolia.py +++ b/tests/units/reflex_site_shared/test_algolia.py @@ -20,7 +20,7 @@ ("rx.data_table", "data table"), ("rx.alert_dialog.root", "alert dialog root"), ("rx.scroll_area", "scroll area"), - (" rx.button ", "button"), + ("rx.button.", "rx.button."), ("RX.BUTTON", "BUTTON"), ("button", "button"), ("radio group", "radio group"), @@ -32,74 +32,40 @@ ], ) def test_algolia_search_normalizes_component_queries( - query: str, expected_query: str + query: str, expected_query: str, tmp_path: Path ) -> None: - """Send component names as title words while preserving input and cache keys. + """Normalize component names through the same module imported by the client. Args: - query: User-entered search text. + query: Trimmed search text. expected_query: Query sent to Algolia. + tmp_path: Temporary directory for the published JavaScript module. """ assets = dict(SharedSiteStylesPlugin().get_static_assets()) - source = assets[Path("public/components/AlgoliaSearch.tsx")] - # Execute the request effect with a fake timer and transport, without React. - effect = source.rsplit("useEffect(() => {", 1)[1].split("}, [isOpen, query]);", 1)[ - 0 - ] - effect = effect.replace(" as AlgoliaResponse", "") + module = tmp_path / "search-query.mjs" + module.write_text( + assets[Path("public/components/search-query.mjs")], encoding="utf-8" + ) script = """ -const query = JSON.parse(process.argv[1]); -const isOpen = true; -const MIN_QUERY_LENGTH = 2; -const MAX_CACHED_QUERIES = 50; -const SEARCH_DEBOUNCE_MS = 350; -const ALGOLIA_SEARCH_ENDPOINT = "https://search.invalid/query"; -const ALGOLIA_APP_ID = "test"; -const ALGOLIA_SEARCH_API_KEY = "test"; -const cacheRef = { current: new Map() }; -const requestRef = { current: null }; -const state = {}; -const setHits = (value) => { state.hits = value; }; -const setNbHits = (value) => { state.nbHits = value; }; -const setStatus = (value) => { state.status = value; }; -const setResultsQuery = (value) => { state.resultsQuery = value; }; -const setActiveIndex = (value) => { state.activeIndex = value; }; -const normalizeHits = (hits) => hits; -const requests = []; -const fetch = async (url, options) => { - requests.push(JSON.parse(options.body)); - return { ok: true, json: async () => ({ hits: [], nbHits: 0 }) }; -}; -let runTimer; -const window = { setTimeout: (callback) => { runTimer = callback; } }; -const runEffect = () => { -""" - script += effect - script += """ -}; -runEffect(); -await runTimer(); -runEffect(); -console.log(JSON.stringify({ - requests, - query, - cacheKeys: [...cacheRef.current.keys()], - state, -})); +import { pathToFileURL } from "node:url"; +const { normalizeSearchQuery } = await import(pathToFileURL(process.argv[1])); +console.log(JSON.stringify(normalizeSearchQuery(JSON.parse(process.argv[2])))); """ result = subprocess.run( - ["node", "--input-type=module", "--eval", script, json.dumps(query)], + [ + "node", + "--input-type=module", + "--eval", + script, + str(module), + json.dumps(query), + ], capture_output=True, encoding="utf-8", timeout=10, check=True, ) - output = json.loads(result.stdout) - assert [request["query"] for request in output["requests"]] == [expected_query] - assert output["query"] == query - assert output["cacheKeys"] == [query.strip().lower()] - assert output["state"]["resultsQuery"] == query.strip().lower() - assert output["state"]["status"] == "ready" + assert json.loads(result.stdout) == expected_query def test_algolia_search_uses_local_keyword_only_component() -> None: diff --git a/tests/units/reflex_site_shared/test_plugins.py b/tests/units/reflex_site_shared/test_plugins.py index 64f2c18e35e..53f56837e0c 100644 --- a/tests/units/reflex_site_shared/test_plugins.py +++ b/tests/units/reflex_site_shared/test_plugins.py @@ -42,6 +42,7 @@ def test_shared_site_styles_plugin_emits_package_css(): Path("public/icons/search.svg"), Path("public/components/marketing-date.jsx"), Path("public/homepage/lib/use-isomorphic-layout-effect.ts"), + Path("public/components/search-query.mjs"), ] assert all(content.strip() for _path, content in assets) assert "ph-conversations-widget" in assets[1][1] From c09593568a09af6f58c4a679e99d662c85b9cfd2 Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Fri, 23 Oct 2026 11:56:16 +0200 Subject: [PATCH 3/5] Cover published docs search normalization wiring --- .../units/reflex_site_shared/test_algolia.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/units/reflex_site_shared/test_algolia.py b/tests/units/reflex_site_shared/test_algolia.py index a74d99d4583..eb04bd4ccae 100644 --- a/tests/units/reflex_site_shared/test_algolia.py +++ b/tests/units/reflex_site_shared/test_algolia.py @@ -1,6 +1,7 @@ """Tests for the shared keyword-only Algolia search.""" import json +import re import shutil import subprocess from pathlib import Path @@ -68,6 +69,26 @@ def test_algolia_search_normalizes_component_queries( assert json.loads(result.stdout) == expected_query +def test_algolia_search_wires_normalization_into_requests() -> None: + """Normalize requests through the published helper while keeping raw cache keys.""" + assets = dict(SharedSiteStylesPlugin().get_static_assets()) + source = assets[Path("public/components/AlgoliaSearch.tsx")] + + assert re.search( + r"import\s*\{\s*normalizeSearchQuery\s*,?\s*\}\s*from\s*" + r"[\"']\./search-query\.mjs[\"']", + source, + ) + assert re.search( + r"\bquery\s*:\s*normalizeSearchQuery\s*\(\s*trimmedQuery\s*,?\s*\)", + source, + ) + assert re.search( + r"\bconst\s+cacheKey\s*=\s*trimmedQuery\.toLocaleLowerCase\(\s*\)", + source, + ) + + def test_algolia_search_uses_local_keyword_only_component() -> None: """Use the local Algolia UI without shipping an AI search dependency.""" assert AlgoliaSearch.library == "$/public/components/AlgoliaSearch" From 91fd78a18662bf37bf38dd5d2d305c093cc74480 Mon Sep 17 00:00:00 2001 From: carlosabadia Date: Wed, 11 Nov 2026 09:57:41 +0100 Subject: [PATCH 4/5] Drop docs search query rewrite; lead API reference titles with rx names Sending rx.* queries raw ranks component pages higher than the rewritten words, and the rewrite returned no results for rx.el.* names. Put the public name in API reference titles instead (rx.State API Reference), the same approach component titles use. Co-Authored-By: Claude Opus 5.5 --- .../news/+api-reference-search-titles.docs.md | 1 + docs/app/reflex_docs/pages/docs/apiref.py | 24 +++++- docs/app/tests/test_routes.py | 17 ++++ packages/reflex-site-shared/README.md | 7 -- .../+normalize-component-search.bugfix.md | 1 - .../src/reflex_site_shared/plugins.py | 1 - .../assets/components/AlgoliaSearch.tsx | 4 +- .../styles/assets/components/search-query.mjs | 11 --- .../units/reflex_site_shared/test_algolia.py | 83 ------------------- .../units/reflex_site_shared/test_plugins.py | 1 - 10 files changed, 40 insertions(+), 110 deletions(-) create mode 100644 docs/app/news/+api-reference-search-titles.docs.md delete mode 100644 packages/reflex-site-shared/news/+normalize-component-search.bugfix.md delete mode 100644 packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs diff --git a/docs/app/news/+api-reference-search-titles.docs.md b/docs/app/news/+api-reference-search-titles.docs.md new file mode 100644 index 00000000000..8cb4ecc9596 --- /dev/null +++ b/docs/app/news/+api-reference-search-titles.docs.md @@ -0,0 +1 @@ +Lead API reference page titles with the public name, such as `rx.State API Reference`, so searching for `rx.State` finds its reference page. diff --git a/docs/app/reflex_docs/pages/docs/apiref.py b/docs/app/reflex_docs/pages/docs/apiref.py index e19310ccb72..98cefc07d89 100644 --- a/docs/app/reflex_docs/pages/docs/apiref.py +++ b/docs/app/reflex_docs/pages/docs/apiref.py @@ -61,6 +61,25 @@ from .env_vars import env_vars_doc + +def public_name(cls: type) -> str: + """Return the name users reach a class by, such as ``rx.State``. + + Search matches the identifier users type, so API reference titles lead + with it, like component titles do. + + Args: + cls: A documented class. + + Returns: + The ``rx`` or ``rx.event`` attribute path, or the bare class name. + """ + for namespace, prefix in ((rx, "rx"), (rx.event, "rx.event")): + if getattr(namespace, cls.__name__, None) is cls: + return f"{prefix}.{cls.__name__}" + return cls.__name__ + + pages = [] for module in modules: name = module.__name__.lower() @@ -72,10 +91,9 @@ f"/api-reference/{name}/", title, source_path=inspect.getsourcefile(module) )(docs) # Keep the short sidebar/nav label (e.g. "App"), but emit a descriptive HTML - # for SEO. Use the real class name (e.g. "ComponentState") so it - # reads as a proper API symbol. + # <title> that leads with the public name (e.g. "rx.State") for search. page_data.title = title - page_data.seo_title = f"{module.__name__} API Reference · Reflex Docs" + page_data.seo_title = f"{public_name(module)} API Reference · Reflex Docs" pages.append(page_data) pages.append(env_vars_doc) diff --git a/docs/app/tests/test_routes.py b/docs/app/tests/test_routes.py index 76e5ff39d26..d26f419a617 100644 --- a/docs/app/tests/test_routes.py +++ b/docs/app/tests/test_routes.py @@ -585,3 +585,20 @@ def test_library_route_titles(routes_fixture, path, expected): """Published library pages carry the final title search engines see.""" titles = {route.path: route.seo_title or route.title for route in routes_fixture} assert titles[path] == expected + + +@pytest.mark.parametrize( + ("path", "expected"), + [ + ("/api-reference/app/", "rx.App API Reference · Reflex Docs"), + ("/api-reference/state/", "rx.State API Reference · Reflex Docs"), + ("/api-reference/var/", "rx.Var API Reference · Reflex Docs"), + ("/api-reference/eventhandler/", "rx.EventHandler API Reference · Reflex Docs"), + ("/api-reference/eventspec/", "rx.event.EventSpec API Reference · Reflex Docs"), + ("/api-reference/statemanager/", "StateManager API Reference · Reflex Docs"), + ], +) +def test_api_reference_route_titles(routes_fixture, path, expected): + """API reference titles lead with the public name users type into search.""" + titles = {route.path: route.seo_title or route.title for route in routes_fixture} + assert titles[path] == expected diff --git a/packages/reflex-site-shared/README.md b/packages/reflex-site-shared/README.md index a710653feac..d04dd97f0ab 100644 --- a/packages/reflex-site-shared/README.md +++ b/packages/reflex-site-shared/README.md @@ -108,13 +108,6 @@ the exported `render_docgen_document` helper directly. Add `defer` to a `python demo exec` fence to mount a heavy preview as it approaches the viewport. The example's source code remains in the initial HTML; the preview reserves 450px of height and stays mounted once shown. This is useful for pages containing many Plotly charts. -### Component search - -The shared search matches complete `rx.*` identifiers to words in documentation -titles: `rx.button` searches for `button`, `rx.radio_group` for `radio group`, -and `rx.alert_dialog.root` for `alert dialog root`. The input keeps the original -text. Ordinary search phrases are sent unchanged. - ## Marketing footer Use the marketing site's responsive five-column directory footer in another diff --git a/packages/reflex-site-shared/news/+normalize-component-search.bugfix.md b/packages/reflex-site-shared/news/+normalize-component-search.bugfix.md deleted file mode 100644 index 98cde988cdd..00000000000 --- a/packages/reflex-site-shared/news/+normalize-component-search.bugfix.md +++ /dev/null @@ -1 +0,0 @@ -Normalize component searches such as `rx.button` and `rx.radio_group` to match documentation titles, improving component page relevance. diff --git a/packages/reflex-site-shared/src/reflex_site_shared/plugins.py b/packages/reflex-site-shared/src/reflex_site_shared/plugins.py index eb34b0d78a1..1ff2dba698c 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/plugins.py +++ b/packages/reflex-site-shared/src/reflex_site_shared/plugins.py @@ -25,7 +25,6 @@ "icons/search.svg", "components/marketing-date.jsx", "homepage/lib/use-isomorphic-layout-effect.ts", - "components/search-query.mjs", ) diff --git a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx index a80284657d2..77cfe3e4e28 100644 --- a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx +++ b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/AlgoliaSearch.tsx @@ -22,8 +22,6 @@ import React, { } from "react"; import { createPortal } from "react-dom"; -import { normalizeSearchQuery } from "./search-query.mjs"; - const ALGOLIA_APP_ID = "WLK9YABRW4"; const ALGOLIA_SEARCH_API_KEY = "7abb638647d3aa25e7a6d29dfdca2212"; const ALGOLIA_INDEX_NAME = "reflex_dev_wlk9yabrw4_pages"; @@ -449,7 +447,7 @@ export function AlgoliaSearch() { "X-Algolia-Application-Id": ALGOLIA_APP_ID, }, body: JSON.stringify({ - query: normalizeSearchQuery(trimmedQuery), + query: trimmedQuery, hitsPerPage: 10, attributesToRetrieve: [ "objectID", diff --git a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs b/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs deleted file mode 100644 index 1b967442d7d..00000000000 --- a/packages/reflex-site-shared/src/reflex_site_shared/styles/assets/components/search-query.mjs +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Convert a complete Reflex component identifier to searchable title words. - * - * @param {string} query A trimmed search query. - * @returns {string} The component's title words, or the unchanged query. - */ -export function normalizeSearchQuery(query) { - return /^rx\.[a-z]\w*(?:\.[a-z]\w*)*$/i.test(query) - ? query.slice(3).replace(/[_.]+/g, " ") - : query; -} diff --git a/tests/units/reflex_site_shared/test_algolia.py b/tests/units/reflex_site_shared/test_algolia.py index eb04bd4ccae..0c21ab5efba 100644 --- a/tests/units/reflex_site_shared/test_algolia.py +++ b/tests/units/reflex_site_shared/test_algolia.py @@ -1,94 +1,11 @@ """Tests for the shared keyword-only Algolia search.""" -import json -import re -import shutil -import subprocess from pathlib import Path -import pytest from reflex_site_shared.components.algolia import AlgoliaSearch, Search from reflex_site_shared.plugins import SharedSiteStylesPlugin -@pytest.mark.skipif(shutil.which("node") is None, reason="node missing") -@pytest.mark.parametrize( - ("query", "expected_query"), - [ - ("rx.button", "button"), - ("rx.input", "input"), - ("rx.radio_group", "radio group"), - ("rx.data_table", "data table"), - ("rx.alert_dialog.root", "alert dialog root"), - ("rx.scroll_area", "scroll area"), - ("rx.button.", "rx.button."), - ("RX.BUTTON", "BUTTON"), - ("button", "button"), - ("radio group", "radio group"), - ("on_click", "on_click"), - ("how to style rx.button", "how to style rx.button"), - ("example.rx.button", "example.rx.button"), - ("https://rx.button", "https://rx.button"), - ("rx.", "rx."), - ], -) -def test_algolia_search_normalizes_component_queries( - query: str, expected_query: str, tmp_path: Path -) -> None: - """Normalize component names through the same module imported by the client. - - Args: - query: Trimmed search text. - expected_query: Query sent to Algolia. - tmp_path: Temporary directory for the published JavaScript module. - """ - assets = dict(SharedSiteStylesPlugin().get_static_assets()) - module = tmp_path / "search-query.mjs" - module.write_text( - assets[Path("public/components/search-query.mjs")], encoding="utf-8" - ) - script = """ -import { pathToFileURL } from "node:url"; -const { normalizeSearchQuery } = await import(pathToFileURL(process.argv[1])); -console.log(JSON.stringify(normalizeSearchQuery(JSON.parse(process.argv[2])))); -""" - result = subprocess.run( - [ - "node", - "--input-type=module", - "--eval", - script, - str(module), - json.dumps(query), - ], - capture_output=True, - encoding="utf-8", - timeout=10, - check=True, - ) - assert json.loads(result.stdout) == expected_query - - -def test_algolia_search_wires_normalization_into_requests() -> None: - """Normalize requests through the published helper while keeping raw cache keys.""" - assets = dict(SharedSiteStylesPlugin().get_static_assets()) - source = assets[Path("public/components/AlgoliaSearch.tsx")] - - assert re.search( - r"import\s*\{\s*normalizeSearchQuery\s*,?\s*\}\s*from\s*" - r"[\"']\./search-query\.mjs[\"']", - source, - ) - assert re.search( - r"\bquery\s*:\s*normalizeSearchQuery\s*\(\s*trimmedQuery\s*,?\s*\)", - source, - ) - assert re.search( - r"\bconst\s+cacheKey\s*=\s*trimmedQuery\.toLocaleLowerCase\(\s*\)", - source, - ) - - def test_algolia_search_uses_local_keyword_only_component() -> None: """Use the local Algolia UI without shipping an AI search dependency.""" assert AlgoliaSearch.library == "$/public/components/AlgoliaSearch" diff --git a/tests/units/reflex_site_shared/test_plugins.py b/tests/units/reflex_site_shared/test_plugins.py index 53f56837e0c..64f2c18e35e 100644 --- a/tests/units/reflex_site_shared/test_plugins.py +++ b/tests/units/reflex_site_shared/test_plugins.py @@ -42,7 +42,6 @@ def test_shared_site_styles_plugin_emits_package_css(): Path("public/icons/search.svg"), Path("public/components/marketing-date.jsx"), Path("public/homepage/lib/use-isomorphic-layout-effect.ts"), - Path("public/components/search-query.mjs"), ] assert all(content.strip() for _path, content in assets) assert "ph-conversations-widget" in assets[1][1] From 94a486ae376ffff68910d1d999db4d669b0dee44 Mon Sep 17 00:00:00 2001 From: carlosabadia <cutillascarlos@gmail.com> Date: Wed, 11 Nov 2026 10:10:56 +0100 Subject: [PATCH 5/5] Make the API reference name helper private Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --- docs/app/reflex_docs/pages/docs/apiref.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/app/reflex_docs/pages/docs/apiref.py b/docs/app/reflex_docs/pages/docs/apiref.py index 98cefc07d89..0f55cb82e81 100644 --- a/docs/app/reflex_docs/pages/docs/apiref.py +++ b/docs/app/reflex_docs/pages/docs/apiref.py @@ -62,7 +62,7 @@ from .env_vars import env_vars_doc -def public_name(cls: type) -> str: +def _public_name(cls: type) -> str: """Return the name users reach a class by, such as ``rx.State``. Search matches the identifier users type, so API reference titles lead @@ -93,7 +93,7 @@ def public_name(cls: type) -> str: # Keep the short sidebar/nav label (e.g. "App"), but emit a descriptive HTML # <title> that leads with the public name (e.g. "rx.State") for search. page_data.title = title - page_data.seo_title = f"{public_name(module)} API Reference · Reflex Docs" + page_data.seo_title = f"{_public_name(module)} API Reference · Reflex Docs" pages.append(page_data) pages.append(env_vars_doc)