This is an automated email from the ASF dual-hosted git repository.
kaxilnaik pushed a commit to branch v3-1-test
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/v3-1-test by this push:
new 7cd78f4cf39 Add fast client-side search to Airflow documentation
(#59658)
7cd78f4cf39 is described below
commit 7cd78f4cf390c46f747dba334a7555d0f7a8591c
Author: Kaxil Naik <[email protected]>
AuthorDate: Sat Dec 20 13:01:09 2025 +0000
Add fast client-side search to Airflow documentation (#59658)
I have been frustrated by Sphinx search for a long-long time. So after
adding dark-mode, this was next in my list!
This PR/commit introduces a fast, fully client-side search experience for
the Apache Airflow documentation, powered by [Pagefind](https://pagefind.app/).
The new search is keyboard-accessible (Cmd+K / Ctrl+K), works offline, and
requires no external services.
Search indexes are generated automatically at documentation build time and
loaded entirely in the browser, enabling sub-50 ms queries even on large docs.
I have kept the Sphinx search too as a backup and it will keep functioning.
----
Add keyboard-accessible search (Cmd+K) to Apache Airflow documentation with
automatic indexing and offline support.
New Sphinx extension: `pagefind_search`
Located in `devel-common/src/sphinx_exts/pagefind_search/`:
- __init__.py: Extension setup with configuration values and event handlers
- builder.py: Automatic index building with graceful fallback
- static/css/pagefind.css: Search modal and button styling with dark mode
support
- static/js/search.js: Search functionality with keyboard shortcuts
- templates/search-modal.html: Search modal HTML template
- Keyboard shortcut (Cmd+K/Ctrl+K) opens search modal
- Arrow key navigation through results
- Works offline (no external services)
- Automatic indexing during documentation build
- Dark mode support
- Sub-50ms search performance
- Configurable content indexing via conf.py
Users can now:
- Press Cmd+K from any documentation page to search
- Navigate results with arrow keys, Enter to select, Esc to close
- Search works immediately without network requests
- Results show page title, breadcrumb, and excerpt
Available in conf.py:
- pagefind_enabled: Toggle search indexing
- pagefind_verbose: Enable build logging
- pagefind_root_selector: Define searchable content area
- pagefind_exclude_selectors: Exclude navigation, headers, footers
- pagefind_custom_records: Index non-HTML content (PDFs, etc.)
(cherry picked from commit d0bd2df6d194a8b923fb55b2f37107642c261b40)
---
Dockerfile | 8 +-
Dockerfile.ci | 8 +-
airflow-core/docs/conf.py | 8 +
devel-common/pyproject.toml | 1 +
devel-common/src/docs/utils/conf_constants.py | 1 +
.../src/sphinx_exts/pagefind_search/README.md | 165 +++++++
.../src/sphinx_exts/pagefind_search/__init__.py | 103 ++++
.../src/sphinx_exts/pagefind_search/builder.py | 215 +++++++++
.../pagefind_search/static/css/pagefind.css | 529 +++++++++++++++++++++
.../pagefind_search/static/js/search.js | 228 +++++++++
.../pagefind_search/templates/search-modal.html | 48 ++
.../pagefind_search/templates/searchbox.html | 33 ++
.../docker/install_airflow_when_building_images.sh | 8 +-
13 files changed, 1349 insertions(+), 6 deletions(-)
diff --git a/Dockerfile b/Dockerfile
index 1c759a6338e..e1eaf7ea885 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1235,9 +1235,13 @@ function install_airflow_when_building_images() {
set +x
common::install_packaging_tools
echo
- echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}"
+ echo "${COLOR_BLUE}Running 'uv pip check'${COLOR_RESET}"
echo
- pip check
+ # Here we should use `pip check` not `uv pip check` to detect any
incompatibilities that might happen
+ # between `pip` and `uv` installations
+ # However, in the current version of `pip` there is a bug that incorrectly
detects `pagefind-bin` as unsupported
+ # https://github.com/pypa/pip/issues/13709 -> once this is fixed, we
should bring `pip check` back.
+ uv pip check
}
common::get_colors
diff --git a/Dockerfile.ci b/Dockerfile.ci
index d9fb8717c14..c6bee261db6 100644
--- a/Dockerfile.ci
+++ b/Dockerfile.ci
@@ -989,9 +989,13 @@ function install_airflow_when_building_images() {
set +x
common::install_packaging_tools
echo
- echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}"
+ echo "${COLOR_BLUE}Running 'uv pip check'${COLOR_RESET}"
echo
- pip check
+ # Here we should use `pip check` not `uv pip check` to detect any
incompatibilities that might happen
+ # between `pip` and `uv` installations
+ # However, in the current version of `pip` there is a bug that incorrectly
detects `pagefind-bin` as unsupported
+ # https://github.com/pypa/pip/issues/13709 -> once this is fixed, we
should bring `pip check` back.
+ uv pip check
}
common::get_colors
diff --git a/airflow-core/docs/conf.py b/airflow-core/docs/conf.py
index c57118e572c..99e0527fcdb 100644
--- a/airflow-core/docs/conf.py
+++ b/airflow-core/docs/conf.py
@@ -268,6 +268,14 @@ global_substitutions = {
"experimental": "This is an :ref:`experimental feature <experimental>`.",
}
+# Pagefind search configuration
+pagefind_exclude_patterns = [
+ "_api/**", # Exclude auto-generated API documentation
+ "_modules/**", # Exclude source code modules
+ "release_notes.html", # Exclude changelog aggregation page
+ "genindex.html", # Exclude generated index
+]
+
# -- Options for sphinx.ext.autodoc
--------------------------------------------
# See: https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html
diff --git a/devel-common/pyproject.toml b/devel-common/pyproject.toml
index 0b3b1ab787c..80cb849b4a5 100644
--- a/devel-common/pyproject.toml
+++ b/devel-common/pyproject.toml
@@ -76,6 +76,7 @@ dependencies = [
"rich-click>=1.7.1",
"click>=8.1.8",
"docutils>=0.21",
+ "pagefind[bin]",
"sphinx-airflow-theme@https://github.com/apache/airflow-site/releases/download/0.3.0/sphinx_airflow_theme-0.3.0-py3-none-any.whl",
"sphinx-argparse>=0.4.0",
"sphinx-autoapi>=3",
diff --git a/devel-common/src/docs/utils/conf_constants.py
b/devel-common/src/docs/utils/conf_constants.py
index 1b8285460e8..77b4426363c 100644
--- a/devel-common/src/docs/utils/conf_constants.py
+++ b/devel-common/src/docs/utils/conf_constants.py
@@ -91,6 +91,7 @@ BASIC_SPHINX_EXTENSIONS = [
"redirects",
"substitution_extensions",
"sphinx_design",
+ "pagefind_search",
]
SPHINX_REDOC_EXTENSIONS = [
diff --git a/devel-common/src/sphinx_exts/pagefind_search/README.md
b/devel-common/src/sphinx_exts/pagefind_search/README.md
new file mode 100644
index 00000000000..b01e3c077ac
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/README.md
@@ -0,0 +1,165 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+ -->
+
+# Pagefind Search Extension
+
+A Sphinx extension providing fast, self-hosted search for Apache Airflow
documentation using [Pagefind](https://pagefind.app/).
+
+## Features
+
+- **Automatic indexing** when docs are built
+- **Cmd+K search** with modern UI and keyboard shortcuts
+- **Self-hosted** - No third-party services
+- **Content weighting** - Prioritizes titles and headings for better relevance
+- **Optimized ranking** - Tuned for exact phrase matching and title matches
+- **Playground support** - Debug and tune search behavior
+
+## Usage
+
+### Search Interface
+
+- **Keyboard**: Press `Cmd+K` (Mac) or `Ctrl+K` (Windows/Linux)
+- **Mouse**: Click the search button in the header
+- **Navigation**: Arrow keys to navigate, Enter to select, Esc to close
+
+## Configuration
+
+In Sphinx's `conf.py`:
+
+```python
+# Enable/disable search (default: True)
+pagefind_enabled = True
+
+# Verbose logging (default: False)
+pagefind_verbose = False
+
+# Content selector (default: "main")
+pagefind_root_selector = "main"
+
+# Exclude selectors (default: see below)
+# These elements won't be included in the search index
+pagefind_exclude_selectors = [
+ ".headerlink", # Permalink icons
+ ".toctree-wrapper", # Table of contents navigation
+ "nav", # All navigation elements
+ "footer", # Footer content
+ ".td-sidebar", # Left sidebar
+ ".breadcrumb", # Breadcrumb navigation
+ ".navbar", # Top navigation bar
+ ".dropdown-menu", # Dropdown menus (version selector, etc.)
+ ".docs-version-selector", # Version selector widget
+ "[role='navigation']", # ARIA navigation landmarks
+ ".d-print-none", # Print-hidden elements (usually UI controls)
+ ".pagefind-search-button", # Search button itself
+]
+
+# File pattern (default: "**/*.html")
+pagefind_glob = "**/*.html"
+
+# Exclude patterns (default: [])
+# Path patterns to exclude from indexing (e.g., auto-generated API docs)
+# Note: File-by-file indexing is used when patterns are specified (slightly
slower but precise)
+# Pagefind does NOT automatically exclude underscore-prefixed directories
+pagefind_exclude_patterns = [
+ "_api/**", # Exclude API documentation
+ "_modules/**", # Exclude source code modules
+ "release_notes.html", # Exclude specific files
+ "genindex.html", # Exclude generated index
+]
+
+# Content weighting (default: True)
+# Uses lightweight regex to add data-pagefind-weight attributes to titles and
headings
+pagefind_content_weighting = True
+
+# Enable playground (default: False)
+# Creates a playground at /_pagefind/playground/ for debugging search
+pagefind_enable_playground = False
+
+# Custom records for non-HTML content (default: [])
+pagefind_custom_records = [
+ {
+ "url": "/downloads/guide.pdf",
+ "content": "PDF content...",
+ "language": "en",
+ "meta": {"title": "Guide PDF"},
+ }
+]
+```
+
+### Ranking Optimization
+
+The extension uses optimized ranking parameters in `search.js`:
+
+- **termFrequency: 1.0** - Standard term occurrence weighting
+- **termSaturation: 0.7** - Moderate saturation to prevent over-rewarding
repetition
+- **termSimilarity: 7.5** - Maximum boost for exact phrase matches and similar
terms
+- **pageLength: 0** - No penalty for longer pages (important for reference
documentation)
+
+Combined with content weighting (10x for titles, 9x for h1, 7x for h2), these
settings ensure exact title matches rank highly even for very long pages.
+
+## Architecture
+
+### Build Process
+
+1. Sphinx builds HTML documentation
+2. Extension copies CSS/JS to `_static/`
+3. Extension injects search modal HTML into pages
+4. (Optional) Extension adds content weights to HTML files
+5. Extension builds Pagefind index with configured options
+
+### Runtime
+
+1. User presses Cmd+K or clicks search button
+2. JavaScript loads Pagefind library dynamically
+3. Search executes client-side
+4. Results rendered in modal
+
+## Troubleshooting
+
+### Search not working
+
+1. Check Pagefind is installed: `python -c "import pagefind; print('OK')"`
+2. Check index exists: `ls
generated/_build/docs/apache-airflow/stable/_pagefind/`
+3. Enable verbose logging: `pagefind_verbose = True` in `conf.py`
+
+### Index not created
+
+- Ensure `pagefind_enabled = True` in `conf.py`
+- Check build logs for errors
+- If `pagefind[bin]` unavailable, use: `npx pagefind --site <build-dir>`
+
+### Poor search ranking
+
+1. Enable playground: `pagefind_enable_playground = True` in `conf.py`
+2. Rebuild docs and access playground at `/_pagefind/playground/`
+3. Test queries and analyze ranking scores
+4. Ensure `pagefind_content_weighting = True` (default)
+5. Check that titles and headings contain expected keywords
+
+### Debugging with Playground
+
+The playground provides detailed insights:
+
+- View all indexed pages
+- See ranking scores for each result
+- Analyze impact of different search terms
+- Verify content weighting is applied
+- Test ranking parameter changes
+
+Example, access at:
`http://localhost:8000/docs/apache-airflow/stable/_pagefind/playground/`
diff --git a/devel-common/src/sphinx_exts/pagefind_search/__init__.py
b/devel-common/src/sphinx_exts/pagefind_search/__init__.py
new file mode 100644
index 00000000000..acfe12ea1d8
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/__init__.py
@@ -0,0 +1,103 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Sphinx extension for Pagefind search integration."""
+
+from __future__ import annotations
+
+from pathlib import Path
+from typing import TYPE_CHECKING, Any
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
+
+from sphinx_exts.pagefind_search.builder import build_index_finished,
copy_static_files
+
+
+def register_templates(app: Sphinx, config) -> None:
+ """Register template directory for searchbox override."""
+ template_dir = str(Path(__file__).parent / "templates")
+ # Prepend so our template overrides Sphinx's default searchbox
+ if template_dir not in config.templates_path:
+ config.templates_path.insert(0, template_dir)
+
+
+def inject_search_html(app: Sphinx, pagename: str, templatename: str, context:
dict, doctree) -> None:
+ """Inject Pagefind search modal HTML into page context."""
+ template_dir = Path(__file__).parent / "templates"
+ modal_file = template_dir / "search-modal.html"
+
+ if not modal_file.exists():
+ return
+
+ modal_content = modal_file.read_text()
+
+ from jinja2 import Template
+
+ modal_template = Template(modal_content)
+ search_modal_html = modal_template.render(pathto=context.get("pathto"))
+
+ context["pagefind_search_modal"] = search_modal_html
+
+ if "body" in context:
+ context["body"] = search_modal_html + context["body"]
+
+
+def setup(app: Sphinx) -> dict[str, Any]:
+ """Setup the Pagefind search extension."""
+ app.add_config_value("pagefind_enabled", True, "html", [bool])
+ app.add_config_value("pagefind_verbose", False, "html", [bool])
+ app.add_config_value("pagefind_root_selector", "main", "html", [str])
+ app.add_config_value(
+ "pagefind_exclude_selectors",
+ [
+ ".headerlink",
+ ".toctree-wrapper",
+ "nav",
+ "footer",
+ ".td-sidebar",
+ ".breadcrumb",
+ ".navbar",
+ ".dropdown-menu",
+ ".docs-version-selector",
+ "[role='navigation']",
+ ".d-print-none",
+ ".pagefind-search-button",
+ ],
+ "html",
+ [list],
+ )
+ app.add_config_value("pagefind_glob", "**/*.html", "html", [str])
+ app.add_config_value("pagefind_exclude_patterns", [], "html", [list])
+ app.add_config_value("pagefind_custom_records", [], "html", [list])
+ app.add_config_value("pagefind_content_weighting", True, "html", [bool])
+ app.add_config_value("pagefind_enable_playground", False, "html", [bool])
+
+ app.add_css_file("css/pagefind.css")
+ app.add_js_file("js/search.js")
+
+ # Register template directory for searchbox override
+ app.connect("config-inited", register_templates)
+ app.connect("html-page-context", inject_search_html)
+ app.connect("build-finished", copy_static_files, priority=100)
+ app.connect("build-finished", build_index_finished, priority=900)
+
+ return {
+ "version": "1.0.0",
+ "parallel_read_safe": True,
+ "parallel_write_safe": False,
+ }
diff --git a/devel-common/src/sphinx_exts/pagefind_search/builder.py
b/devel-common/src/sphinx_exts/pagefind_search/builder.py
new file mode 100644
index 00000000000..066123bd980
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/builder.py
@@ -0,0 +1,215 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+"""Pagefind index builder and static file handler."""
+
+from __future__ import annotations
+
+import asyncio
+import logging
+import re
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+from pagefind.index import IndexConfig, PagefindIndex
+from sphinx.util.fileutil import copy_asset
+
+if TYPE_CHECKING:
+ from sphinx.application import Sphinx
+
+logger = logging.getLogger(__name__)
+
+
+def add_content_weights_lightweight(
+ output_dir: Path, glob_pattern: str, exclude_patterns: list[str] | None =
None
+) -> int:
+ """Add data-pagefind-weight attributes using simple regex replacement.
+
+ :param output_dir: Output directory
+ :param glob_pattern: Glob pattern
+ :param exclude_patterns: Exclude patterns
+ :return: Number of files processed
+ """
+ files_processed = 0
+ exclude_patterns = exclude_patterns or []
+
+ # Regex patterns to match opening tags without existing weight attribute
+ # Use maximum valid weights (0.0-10.0 range, quadratic scale)
+ # https://pagefind.app/docs/weighting/
+ # Weight of 10.0 = ~100x impact, 7.0 = ~49x impact (default h1), 5.0 =
~25x impact
+ patterns = [
+ (re.compile(r"<title(?![^>]*data-pagefind-weight)"), '<title
data-pagefind-weight="10.0"'),
+ (re.compile(r"<h1(?![^>]*data-pagefind-weight)"), '<h1
data-pagefind-weight="9.0"'),
+ ]
+
+ for html_file in output_dir.glob(glob_pattern):
+ if not html_file.is_file():
+ continue
+
+ # Check if file matches any exclude pattern (using simple prefix
matching)
+ relative_path = html_file.relative_to(output_dir)
+ relative_str = str(relative_path)
+ if any(relative_str.startswith(pattern.rstrip("/*")) for pattern in
exclude_patterns):
+ continue
+
+ try:
+ content = html_file.read_text(encoding="utf-8")
+ modified_content = content
+
+ for pattern, replacement in patterns:
+ modified_content = pattern.sub(replacement, modified_content)
+
+ if modified_content != content:
+ html_file.write_text(modified_content, encoding="utf-8")
+ files_processed += 1
+
+ except Exception as e:
+ logger.warning("Failed to add weights to %s: %s", html_file, e)
+
+ return files_processed
+
+
+async def build_pagefind_index(app: Sphinx) -> dict[str, int]:
+ """Build Pagefind search index using Python API."""
+ output_dir = Path(app.builder.outdir)
+ pagefind_dir = output_dir / "_pagefind"
+
+ # Add content weighting if enabled
+ if getattr(app.config, "pagefind_content_weighting", True):
+ logger.info("Adding content weights to HTML files...")
+ exclude_patterns = getattr(app.config, "pagefind_exclude_patterns", [])
+ files_processed = add_content_weights_lightweight(
+ output_dir, app.config.pagefind_glob, exclude_patterns
+ )
+ logger.info("Added content weights to %s files", files_processed)
+
+ config = IndexConfig(
+ root_selector=app.config.pagefind_root_selector,
+ exclude_selectors=app.config.pagefind_exclude_selectors,
+ output_path=str(pagefind_dir),
+ verbose=app.config.pagefind_verbose,
+ force_language=app.config.language or "en",
+ keep_index_url=False,
+ write_playground=getattr(app.config, "pagefind_enable_playground",
False),
+ )
+
+ logger.info("Building Pagefind search index...")
+
+ exclude_patterns = getattr(app.config, "pagefind_exclude_patterns", [])
+
+ if exclude_patterns:
+ # Need to index files individually to apply exclusion patterns
+ logger.info("Indexing with exclusion patterns: %s", exclude_patterns)
+ indexed = 0
+ skipped = 0
+
+ async with PagefindIndex(config=config) as index:
+ for html_file in output_dir.glob(app.config.pagefind_glob):
+ if not html_file.is_file():
+ continue
+
+ relative_path = html_file.relative_to(output_dir)
+ relative_str = str(relative_path)
+
+ # Check if path matches any exclude pattern (prefix matching)
+ if any(relative_str.startswith(pattern.rstrip("/*")) for
pattern in exclude_patterns):
+ skipped += 1
+ continue
+
+ try:
+ content = html_file.read_text(encoding="utf-8")
+ await index.add_html_file(
+ content=content,
+ source_path=str(html_file),
+ url=str(relative_path),
+ )
+ indexed += 1
+ except Exception as e:
+ logger.warning("Failed to index %s: %s", relative_path, e)
+
+ logger.info("Pagefind indexed %s pages (excluded %s)", indexed,
skipped)
+
+ if app.config.pagefind_custom_records:
+ for record in app.config.pagefind_custom_records:
+ try:
+ await index.add_custom_record(**record)
+ except Exception as e:
+ logger.warning("Failed to add custom record: %s", e)
+
+ return {"page_count": indexed}
+ else:
+ # No exclusions - use fast directory indexing
+ async with PagefindIndex(config=config) as index:
+ result = await index.add_directory(path=str(output_dir),
glob=app.config.pagefind_glob)
+ page_count = result.get("page_count", 0)
+ logger.info("Pagefind indexed %s pages", page_count)
+
+ if app.config.pagefind_custom_records:
+ for record in app.config.pagefind_custom_records:
+ try:
+ await index.add_custom_record(**record)
+ except Exception as e:
+ logger.warning("Failed to add custom record: %s", e)
+
+ return {"page_count": page_count}
+
+
+def build_index_finished(app: Sphinx, exception: Exception | None) -> None:
+ """Build Pagefind index after HTML build completes."""
+ if exception:
+ logger.info("Skipping Pagefind indexing due to build errors")
+ return
+
+ if app.builder.format != "html":
+ return
+
+ if not app.config.pagefind_enabled:
+ logger.info("Pagefind indexing disabled (pagefind_enabled=False)")
+ return
+
+ try:
+ result = asyncio.run(build_pagefind_index(app))
+ page_count = result.get("page_count", 0)
+
+ if page_count == 0:
+ raise RuntimeError("Pagefind indexing failed: no pages were
indexed")
+
+ logger.info("✓ Pagefind index created with %s pages", page_count)
+ except Exception as e:
+ logger.exception("Failed to build Pagefind index")
+ raise RuntimeError(f"Pagefind indexing failed: {e}") from e
+
+
+def copy_static_files(app: Sphinx, exception: Exception | None) -> None:
+ """Copy CSS and JS files to _static directory."""
+ if exception or app.builder.format != "html":
+ return
+
+ static_dir = Path(app.builder.outdir) / "_static"
+ extension_static = Path(__file__).parent / "static"
+
+ css_src = extension_static / "css" / "pagefind.css"
+ css_dest = static_dir / "css"
+ css_dest.mkdir(parents=True, exist_ok=True)
+ if css_src.exists():
+ copy_asset(str(css_src), str(css_dest))
+
+ js_src = extension_static / "js" / "search.js"
+ js_dest = static_dir / "js"
+ js_dest.mkdir(parents=True, exist_ok=True)
+ if js_src.exists():
+ copy_asset(str(js_src), str(js_dest))
diff --git
a/devel-common/src/sphinx_exts/pagefind_search/static/css/pagefind.css
b/devel-common/src/sphinx_exts/pagefind_search/static/css/pagefind.css
new file mode 100644
index 00000000000..aa72e9440fb
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/static/css/pagefind.css
@@ -0,0 +1,529 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+/*
+ * Pagefind Search Modal - Cmd+K Search
+ * Minimal styling for search functionality only
+ */
+
+/* Search Modal */
+.search-modal {
+ position: fixed;
+ inset: 0;
+ z-index: 9999;
+ display: none;
+ align-items: flex-start;
+ padding-top: 15vh;
+}
+
+.search-modal.active {
+ display: flex;
+}
+
+.search-modal__backdrop {
+ position: absolute;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.6);
+ backdrop-filter: blur(4px);
+ cursor: pointer;
+}
+
+.search-modal__container {
+ position: relative;
+ width: 90%;
+ max-width: 600px;
+ margin: 0 auto;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+ overflow: hidden;
+ border: 1px solid #dee2e6;
+}
+
+[data-bs-theme="dark"] .search-modal__container {
+ background: #161b22;
+ border-color: #30363d;
+}
+
+.search-modal__header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ padding: 16px 20px;
+ border-bottom: 1px solid #dee2e6;
+}
+
+[data-bs-theme="dark"] .search-modal__header {
+ border-bottom-color: #30363d;
+}
+
+.search-modal__icon {
+ color: #6c757d;
+ flex-shrink: 0;
+ width: 18px;
+ height: 18px;
+}
+
+.search-modal__input {
+ flex: 1;
+ font-size: 16px;
+ border: none;
+ background: transparent;
+ color: #1a1a1a;
+ font-family: inherit;
+}
+
+[data-bs-theme="dark"] .search-modal__input {
+ color: #e6edf3;
+}
+
+.search-modal__input:focus {
+ outline: none;
+}
+
+.search-modal__input::placeholder {
+ color: #adb5bd;
+}
+
+[data-bs-theme="dark"] .search-modal__input::placeholder {
+ color: #6e7681;
+}
+
+.search-modal__close {
+ background: transparent;
+ border: none;
+ color: #6c757d;
+ cursor: pointer;
+ padding: 6px;
+ border-radius: 4px;
+ transition: background 0.2s;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 28px;
+}
+
+.search-modal__close:hover {
+ background: #f8f9fa;
+}
+
+[data-bs-theme="dark"] .search-modal__close {
+ color: #8b949e;
+}
+
+[data-bs-theme="dark"] .search-modal__close:hover {
+ background: #21262d;
+}
+
+.search-modal__close svg {
+ width: 16px;
+ height: 16px;
+}
+
+.search-modal__results {
+ max-height: 400px;
+ overflow-y: auto;
+ padding: 8px;
+}
+
+.search-modal__result-item {
+ padding: 10px 12px;
+ border-radius: 4px;
+ cursor: pointer;
+ display: flex;
+ gap: 10px;
+ text-decoration: none;
+ color: inherit;
+ transition: background 0.2s;
+ margin-bottom: 4px;
+}
+
+.search-modal__result-item:hover,
+.search-modal__result-item--selected {
+ background: #f8f9fa;
+}
+
+[data-bs-theme="dark"] .search-modal__result-item:hover,
+[data-bs-theme="dark"] .search-modal__result-item--selected {
+ background: #21262d;
+}
+
+.search-modal__result-icon {
+ color: #6c757d;
+ flex-shrink: 0;
+ margin-top: 2px;
+ width: 14px;
+ height: 14px;
+}
+
+.search-modal__result-icon svg {
+ width: 14px;
+ height: 14px;
+}
+
+.search-modal__result-content {
+ flex: 1;
+ min-width: 0;
+}
+
+.search-modal__result-title {
+ font-weight: 600;
+ color: #1a1a1a;
+ margin-bottom: 2px;
+ font-size: 14px;
+}
+
+[data-bs-theme="dark"] .search-modal__result-title {
+ color: #e6edf3;
+}
+
+.search-modal__result-breadcrumb {
+ font-size: 11px;
+ color: #6c757d;
+ margin-bottom: 4px;
+}
+
+[data-bs-theme="dark"] .search-modal__result-breadcrumb {
+ color: #8b949e;
+}
+
+.search-modal__result-excerpt {
+ font-size: 12px;
+ color: #6c757d;
+ line-height: 1.5;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ display: -webkit-box;
+ -webkit-line-clamp: 2;
+ -webkit-box-orient: vertical;
+}
+
+[data-bs-theme="dark"] .search-modal__result-excerpt {
+ color: #8b949e;
+}
+
+.search-modal__no-results {
+ text-align: center;
+ padding: 48px 24px;
+ color: #6c757d;
+}
+
+[data-bs-theme="dark"] .search-modal__no-results {
+ color: #8b949e;
+}
+
+.search-modal__no-results p:first-child {
+ font-size: 15px;
+ font-weight: 600;
+ color: #1a1a1a;
+ margin-bottom: 8px;
+}
+
+[data-bs-theme="dark"] .search-modal__no-results p:first-child {
+ color: #e6edf3;
+}
+
+.search-modal__no-results-hint {
+ font-size: 13px;
+ color: #adb5bd;
+}
+
+[data-bs-theme="dark"] .search-modal__no-results-hint {
+ color: #6e7681;
+}
+
+.search-modal__footer {
+ padding: 10px 16px;
+ border-top: 1px solid #dee2e6;
+ background: #f8f9fa;
+}
+
+[data-bs-theme="dark"] .search-modal__footer {
+ border-top-color: #30363d;
+ background: #0d1117;
+}
+
+.search-modal__shortcuts {
+ display: flex;
+ gap: 16px;
+ justify-content: center;
+ font-size: 12px;
+ color: #495057;
+ font-weight: 500;
+}
+
+[data-bs-theme="dark"] .search-modal__shortcuts {
+ color: #c9d1d9;
+}
+
+.search-modal__shortcuts span {
+ display: flex;
+ align-items: center;
+ gap: 6px;
+}
+
+.search-modal__shortcuts kbd {
+ padding: 3px 8px;
+ background: white;
+ border: 1px solid #dee2e6;
+ border-radius: 4px;
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+ margin: 0;
+ font-size: 11px;
+ font-weight: 600;
+ color: #495057;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+[data-bs-theme="dark"] .search-modal__shortcuts kbd {
+ background: #21262d;
+ border-color: #30363d;
+ color: #c9d1d9;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);
+}
+
+/* ============================================
+ Search Button in Header - FIXED POSITIONING
+ ============================================ */
+
+.search-button {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 6px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.1);
+ color: #dee2e6;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ font-size: 14px;
+ font-family: inherit;
+ height: 36px;
+ margin-left: 16px;
+}
+
+.search-button:hover {
+ background: rgba(255, 255, 255, 0.15);
+ border-color: rgba(255, 255, 255, 0.3);
+ color: white;
+}
+
+.search-button__icon {
+ flex-shrink: 0;
+ width: 16px;
+ height: 16px;
+}
+
+.search-button__text {
+ font-weight: 400;
+ font-size: 14px;
+ color: inherit;
+}
+
+.search-button__shortcut {
+ display: flex;
+ gap: 2px;
+ margin-left: 8px;
+ padding: 2px 6px;
+ background: rgba(255, 255, 255, 0.15);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 4px;
+}
+
+.search-button__shortcut span {
+ font-size: 11px;
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+ font-weight: 600;
+ line-height: 1.2;
+ color: rgba(255, 255, 255, 0.9);
+}
+
+/* Light theme adjustments */
+[data-bs-theme="light"] .search-button {
+ border-color: #dee2e6;
+ background: white;
+ color: #495057;
+}
+
+[data-bs-theme="light"] .search-button:hover {
+ background: #f8f9fa;
+ border-color: #adb5bd;
+ color: #212529;
+}
+
+[data-bs-theme="light"] .search-button__shortcut {
+ background: #e9ecef;
+ border-color: #adb5bd;
+}
+
+[data-bs-theme="light"] .search-button__shortcut span {
+ color: #495057;
+ font-weight: 600;
+}
+
+@media (max-width: 768px) {
+ .search-button__text,
+ .search-button__shortcut {
+ display: none;
+ }
+
+ .search-button {
+ padding: 6px;
+ width: 36px;
+ justify-content: center;
+ margin-left: 8px;
+ }
+}
+
+/* Floating Search Button (Fallback) */
+.pagefind-search-button {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ z-index: 9998;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 14px;
+ background: #017cee;
+ color: white;
+ border: 1px solid rgba(1, 124, 238, 0.2);
+ border-radius: 6px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ cursor: pointer;
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
+ font-size: 14px;
+ font-weight: 500;
+ transition: all 0.2s ease;
+}
+
+.pagefind-search-button:hover {
+ background: #0166c7;
+ box-shadow: 0 4px 12px rgba(1, 124, 238, 0.25);
+ transform: translateY(-1px);
+}
+
+.pagefind-search-button__icon {
+ flex-shrink: 0;
+ width: 18px;
+ height: 18px;
+}
+
+.pagefind-search-button__text {
+ color: white;
+ font-weight: 500;
+}
+
+.pagefind-search-button__shortcut {
+ display: flex;
+ gap: 2px;
+ padding: 3px 7px;
+ background: rgba(255, 255, 255, 0.2);
+ border: 1px solid rgba(255, 255, 255, 0.3);
+ border-radius: 4px;
+ font-family: 'SF Mono', 'Monaco', 'Inconsolata', 'Roboto Mono', monospace;
+ font-size: 11px;
+ font-weight: 600;
+ line-height: 1;
+}
+
+.pagefind-search-button__shortcut span {
+ color: white;
+}
+
+@media (max-width: 768px) {
+ .pagefind-search-button__text,
+ .pagefind-search-button__shortcut {
+ display: none;
+ }
+
+ .pagefind-search-button {
+ width: 44px;
+ height: 44px;
+ padding: 10px;
+ justify-content: center;
+ }
+}
+
+/* Sidebar Search Button - Replaces Sphinx searchbox */
+.sidebar-search-container {
+ padding: 0;
+ margin-bottom: 1rem;
+}
+
+.sidebar-search-button {
+ position: static;
+ width: 100%;
+ justify-content: center;
+ padding: 10px 12px;
+ border-radius: 6px;
+}
+
+.sidebar-search-button .pagefind-search-button__text {
+ flex: 1;
+}
+
+/* Light mode - subtle styling */
+[data-bs-theme="light"] .sidebar-search-button {
+ background: white;
+ border-color: #dee2e6;
+ color: #495057;
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
+}
+
+[data-bs-theme="light"] .sidebar-search-button:hover {
+ background: #f8f9fa;
+ border-color: #adb5bd;
+ color: #212529;
+}
+
+[data-bs-theme="light"] .sidebar-search-button .pagefind-search-button__icon {
+ stroke: #6c757d;
+}
+
+[data-bs-theme="light"] .sidebar-search-button .pagefind-search-button__text {
+ color: #495057;
+}
+
+[data-bs-theme="light"] .sidebar-search-button
.pagefind-search-button__shortcut {
+ background: #e9ecef;
+ border-color: #adb5bd;
+}
+
+[data-bs-theme="light"] .sidebar-search-button
.pagefind-search-button__shortcut span {
+ color: #495057;
+}
+
+/* Dark mode - keep blue for visibility */
+[data-bs-theme="dark"] .sidebar-search-button {
+ background: #0166c7;
+}
+
+[data-bs-theme="dark"] .sidebar-search-button:hover {
+ background: #017cee;
+}
+
+/* Hide floating button when sidebar button exists */
+body:has(header .search-button) .pagefind-search-button,
+body:has(nav .search-button) .pagefind-search-button,
+body:has(.sidebar-search-button)
.pagefind-search-button:not(.sidebar-search-button) {
+ display: none;
+}
diff --git a/devel-common/src/sphinx_exts/pagefind_search/static/js/search.js
b/devel-common/src/sphinx_exts/pagefind_search/static/js/search.js
new file mode 100644
index 00000000000..4cffcd11c3a
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/static/js/search.js
@@ -0,0 +1,228 @@
+/*!
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+let pagefind = null;
+let searchResults = [];
+let selectedIndex = 0;
+
+async function initPagefind() {
+ if (!pagefind) {
+ try {
+ const pagefindPath = window.PAGEFIND_PATH || './_pagefind/pagefind.js';
+ const absoluteUrl = new URL(pagefindPath, document.baseURI ||
window.location.href).href;
+
+ const pf = await import(/* webpackIgnore: true */ absoluteUrl);
+ pagefind = pf;
+ await pagefind.options({
+ excerptLength: 15,
+ ranking: {
+ termFrequency: 1.0,
+ termSaturation: 0.7,
+ termSimilarity: 7.5, // Maximum boost for exact/similar matches
+ pageLength: 0 // No penalty for long pages
+ }
+ });
+ } catch (e) {
+ console.error('Failed to load Pagefind:', e);
+ displaySearchError('Search index not available. Please rebuild the
documentation.');
+ return null;
+ }
+ }
+ return pagefind;
+}
+
+function displaySearchError(message) {
+ const resultsDiv = document.getElementById('search-results');
+ if (resultsDiv) {
+ resultsDiv.innerHTML = `
+ <div class="search-modal__no-results">
+ <p>${message}</p>
+ </div>
+ `;
+ }
+}
+
+function openSearch() {
+ const modal = document.getElementById('search-modal');
+ const input = document.getElementById('search-input');
+ if (modal && input) {
+ modal.style.display = 'flex';
+ input.focus();
+ document.body.style.overflow = 'hidden';
+ }
+}
+
+function closeSearch() {
+ const modal = document.getElementById('search-modal');
+ const input = document.getElementById('search-input');
+ const results = document.getElementById('search-results');
+
+ if (modal) modal.style.display = 'none';
+ if (input) input.value = '';
+ if (results) results.innerHTML = '';
+
+ document.body.style.overflow = '';
+ searchResults = [];
+ selectedIndex = 0;
+}
+
+function debounce(func, wait) {
+ let timeout;
+ return function executedFunction(...args) {
+ const later = () => {
+ clearTimeout(timeout);
+ func(...args);
+ };
+ clearTimeout(timeout);
+ timeout = setTimeout(later, wait);
+ };
+}
+
+async function performSearch(query) {
+ const resultsContainer = document.getElementById('search-results');
+ if (!resultsContainer) return;
+
+ if (!query || query.length < 2) {
+ resultsContainer.innerHTML = '';
+ return;
+ }
+
+ const pf = await initPagefind();
+ if (!pf) {
+ resultsContainer.innerHTML = `
+ <div class="search-modal__no-results">
+ <p>Search index not available</p>
+ <p class="search-modal__no-results-hint">Index is built automatically
during 'make html'</p>
+ </div>
+ `;
+ return;
+ }
+
+ try {
+ const search = await pf.search(query);
+ searchResults = await Promise.all(
+ search.results.slice(0, 10).map(r => r.data())
+ );
+
+ renderResults(searchResults);
+ } catch (e) {
+ console.error('Search error:', e);
+ resultsContainer.innerHTML = `
+ <div class="search-modal__no-results">
+ <p>Search temporarily unavailable</p>
+ <p class="search-modal__no-results-hint">Please try again later</p>
+ </div>
+ `;
+ }
+}
+
+function renderResults(results) {
+ const container = document.getElementById('search-results');
+ if (!container) return;
+
+ if (results.length === 0) {
+ container.innerHTML = `
+ <div class="search-modal__no-results">
+ <p>No results found</p>
+ <p class="search-modal__no-results-hint">Try different keywords or
check spelling</p>
+ </div>
+ `;
+ return;
+ }
+
+ container.innerHTML = results.map((result, index) => `
+ <a
+ href="${result.url}"
+ class="search-modal__result-item ${index === selectedIndex ?
'search-modal__result-item--selected' : ''}"
+ data-index="${index}"
+ role="option"
+ aria-selected="${index === selectedIndex}"
+ >
+ <div class="search-modal__result-icon">
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
+ <path d="M1 2.5A1.5 1.5 0 0 1 2.5 1h11A1.5 1.5 0 0 1 15 2.5v11a1.5
1.5 0 0 1-1.5 1.5h-11A1.5 1.5 0 0 1 1 13.5v-11zM2.5 2a.5.5 0 0 0-.5.5v11a.5.5 0
0 0 .5.5h11a.5.5 0 0 0 .5-.5v-11a.5.5 0 0 0-.5-.5h-11z"/>
+ <path d="M4 5.5a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0
1-.5-.5zm0 3a.5.5 0 0 1 .5-.5h7a.5.5 0 0 1 0 1h-7a.5.5 0 0 1-.5-.5zm0 3a.5.5 0
0 1 .5-.5h4a.5.5 0 0 1 0 1h-4a.5.5 0 0 1-.5-.5z"/>
+ </svg>
+ </div>
+ <div class="search-modal__result-content">
+ <div class="search-modal__result-title">${result.meta?.title ||
'Untitled'}</div>
+ <div
class="search-modal__result-breadcrumb">${result.url.replace(/^\/docs\//,
'').replace(/\.html$/, '')}</div>
+ ${result.excerpt ? `<div
class="search-modal__result-excerpt">${result.excerpt}</div>` : ''}
+ </div>
+ </a>
+ `).join('');
+}
+
+function handleKeyboardNav(e) {
+ if (e.key === 'ArrowDown') {
+ e.preventDefault();
+ selectedIndex = Math.min(selectedIndex + 1, searchResults.length - 1);
+ renderResults(searchResults);
+ scrollToSelected();
+ } else if (e.key === 'ArrowUp') {
+ e.preventDefault();
+ selectedIndex = Math.max(selectedIndex - 1, 0);
+ renderResults(searchResults);
+ scrollToSelected();
+ } else if (e.key === 'Enter' && searchResults.length > 0) {
+ e.preventDefault();
+ window.location.href = searchResults[selectedIndex].url;
+ } else if (e.key === 'Escape') {
+ closeSearch();
+ }
+}
+
+function scrollToSelected() {
+ const selected =
document.querySelector('.search-modal__result-item--selected');
+ if (selected) {
+ selected.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+ }
+}
+
+document.addEventListener('DOMContentLoaded', () => {
+ document.addEventListener('keydown', (e) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
+ e.preventDefault();
+ openSearch();
+ }
+ });
+
+ const searchInput = document.getElementById('search-input');
+ if (searchInput) {
+ searchInput.addEventListener('input', debounce((e) => {
+ selectedIndex = 0;
+ performSearch(e.target.value);
+ }, 150));
+
+ searchInput.addEventListener('keydown', handleKeyboardNav);
+ }
+
+ const backdrop = document.querySelector('.search-modal__backdrop');
+ if (backdrop) {
+ backdrop.addEventListener('click', closeSearch);
+ }
+
+ const closeButton = document.querySelector('.search-modal__close');
+ if (closeButton) {
+ closeButton.addEventListener('click', closeSearch);
+ }
+});
+
+window.openSearch = openSearch;
+window.closeSearch = closeSearch;
diff --git
a/devel-common/src/sphinx_exts/pagefind_search/templates/search-modal.html
b/devel-common/src/sphinx_exts/pagefind_search/templates/search-modal.html
new file mode 100644
index 00000000000..b53a1f26a81
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/templates/search-modal.html
@@ -0,0 +1,48 @@
+{#
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+#}
+
+<script>
+window.PAGEFIND_PATH = '{{ pathto("", 1) }}_pagefind/pagefind.js';
+</script>
+
+<div id="search-modal" class="search-modal" role="dialog" aria-modal="true"
aria-label="Search documentation">
+ <div class="search-modal__backdrop"></div>
+ <div class="search-modal__container">
+ <div class="search-modal__header">
+ <svg class="search-modal__icon" width="20" height="20" viewBox="0
0 20 20" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
+ <circle cx="9" cy="9" r="7"></circle>
+ <line x1="14" y1="14" x2="19" y2="19"></line>
+ </svg>
+ <input type="text" id="search-input" class="search-modal__input"
placeholder="Search documentation..." aria-label="Search documentation input">
+ <button class="search-modal__close" onclick="closeSearch()"
aria-label="Close search">
+ <svg width="16" height="16" viewBox="0 0 16 16"
fill="currentColor">
+ <path d="M14.354 1.646a.5.5 0 0 0-.708 0L8 7.293 2.354
1.646a.5.5 0 0 0-.708.708L7.293 8l-5.647 5.646a.5.5 0 0 0 .708.708L8
8.707l5.646 5.647a.5.5 0 0 0 .708-.708L8.707 8l5.647-5.646a.5.5 0 0 0 0-.708z"/>
+ </svg>
+ </button>
+ </div>
+ <div id="search-results" class="search-modal__results" role="listbox"
aria-live="polite"></div>
+ <div class="search-modal__footer">
+ <div class="search-modal__shortcuts">
+ <span><kbd>↑↓</kbd> Navigate</span>
+ <span><kbd>⏎</kbd> Select</span>
+ <span><kbd>Esc</kbd> Close</span>
+ </div>
+ </div>
+ </div>
+</div>
diff --git
a/devel-common/src/sphinx_exts/pagefind_search/templates/searchbox.html
b/devel-common/src/sphinx_exts/pagefind_search/templates/searchbox.html
new file mode 100644
index 00000000000..0c3ec660b09
--- /dev/null
+++ b/devel-common/src/sphinx_exts/pagefind_search/templates/searchbox.html
@@ -0,0 +1,33 @@
+{#
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+#}
+
+{# Override default Sphinx searchbox with Pagefind button #}
+<div class="sidebar-search-container">
+ <button class="pagefind-search-button sidebar-search-button"
onclick="openSearch()" aria-label="Search documentation" title="Search
documentation (Cmd+K or Ctrl+K)">
+ <svg class="pagefind-search-button__icon" width="18" height="18"
viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2"
stroke-linecap="round" stroke-linejoin="round">
+ <circle cx="9" cy="9" r="7"></circle>
+ <line x1="14" y1="14" x2="19" y2="19"></line>
+ </svg>
+ <span class="pagefind-search-button__text">Search docs</span>
+ <kbd class="pagefind-search-button__shortcut">
+ <span>⌘</span>
+ <span>K</span>
+ </kbd>
+ </button>
+</div>
diff --git a/scripts/docker/install_airflow_when_building_images.sh
b/scripts/docker/install_airflow_when_building_images.sh
index c93c0387a63..fb7c1d86db1 100644
--- a/scripts/docker/install_airflow_when_building_images.sh
+++ b/scripts/docker/install_airflow_when_building_images.sh
@@ -200,9 +200,13 @@ function install_airflow_when_building_images() {
set +x
common::install_packaging_tools
echo
- echo "${COLOR_BLUE}Running 'pip check'${COLOR_RESET}"
+ echo "${COLOR_BLUE}Running 'uv pip check'${COLOR_RESET}"
echo
- pip check
+ # Here we should use `pip check` not `uv pip check` to detect any
incompatibilities that might happen
+ # between `pip` and `uv` installations
+ # However, in the current version of `pip` there is a bug that incorrectly
detects `pagefind-bin` as unsupported
+ # https://github.com/pypa/pip/issues/13709 -> once this is fixed, we
should bring `pip check` back.
+ uv pip check
}
common::get_colors