Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,15 @@ jobs:
- name: Build the docs
run: make html


- name: Setup Node.js (Pagefind)
uses: actions/setup-node@v4
with:
node-version: '24'

- name: Index HTML with Pagefind
run: make pagefind

- name: Upload document artifacts
uses: actions/upload-artifact@v4
id: artifact-upload-step
Expand Down Expand Up @@ -147,3 +156,12 @@ jobs:

- name: Build the docs
run: make multiversion


- name: Setup Node.js (Pagefind)
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Index HTML with Pagefind
run: make pagefind
16 changes: 15 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ multiversion: Makefile
@echo "<html><head><meta http-equiv=\"refresh\" content=\"0; url=kilted/index.html\" /></head></html>" > build/html/index.html
$(PYTHON) make_sitemapindex.py

# Pagefind static search index (requires Node.js / npx). Run after html or multiversion.
PAGEFIND_VERSION ?= 1.5.2
pagefind:
npx -y pagefind@$(PAGEFIND_VERSION) --site "$(OUT)/html"


# Convenience: Sphinx build + Pagefind index (does not replace plain html / multiversion).
html-search:
$(MAKE) html
$(MAKE) pagefind

multiversion-search: multiversion
$(MAKE) pagefind

%: Makefile
@$(BUILD) -M $@ "$(SOURCE)" "$(OUT)" $(OPTS)

Expand Down Expand Up @@ -64,4 +78,4 @@ linkcheck:
@echo
@echo "Check finished. Report is in $(LINKCHECKDIR)."

.PHONY: help Makefile multiversion test test-tools linkcheck lint spellcheck check-dictionaries sort-dictionaries
.PHONY: help Makefile multiversion pagefind test test-tools linkcheck lint spellcheck check-dictionaries sort-dictionaries
18 changes: 18 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,24 @@ To test building the multisite version deployed to the website use:

**NB:** This will ignore local workspace changes and build from the branches.

### Pagefind search index

After `make html` or `make multiversion`, run [Pagefind](https://pagefind.app/) so the built HTML under `build/html` is indexed and `build/html/pagefind/` is written (search bundle and Component UI assets). From the repo root:

`make pagefind`

Or use convenience targets that run Sphinx and Pagefind in one step:

- `make html-search` — `make html` then `make pagefind`
- `make multiversion-search` — `make multiversion` then `make pagefind`

Plain `make html` and `make multiversion` do **not** run Pagefind (Node.js is only required when you index search).

This requires **Node.js** (for `npx`). Pin the CLI with `PAGEFIND_VERSION` in the Makefile if needed.

The production [Jenkins doc job](https://build.ros.org/job/doc_ros2doc) should run the same `pagefind` step on `build/html` after Sphinx so deployed pages include the search bundle.


### Note for Windows (WSL) Users

When building the documentation on windows using WSL, it is recommended to clone and work with this repository inside the Linux filesystem (for example, under `/home/<user>/`) rather than under `/mnt/c`.
Expand Down
29 changes: 25 additions & 4 deletions conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,29 @@
'sphinx_adopters',
'sphinxcontrib.googleanalytics',
'sphinxcontrib.mermaid',
'ros_related_packages',
'ros_related_articles',
'short_description',
'pagefind_meta',
'showmeta',
]

pagefind_merge_enabled = False
pagefind_merge_package_pkgs = []
pagefind_merge_index_base = 'https://docs.ros.org'
pagefind_merge_index_overrides = {}
pagefind_merge_filter_per_pkg = None
pagefind_merge_index_weight_per_pkg = None

pagefind_filter_labels = {
'contentType': 'Content type',
}

pagefind_result_meta_order = [
'product',
'distro',
'area',
'capability',
'contentType',
'experience',

]

# Intersphinx mapping
Expand Down Expand Up @@ -195,6 +215,7 @@
'DISTRO_UBUNTU_DEB_PLATFORM': distro_ubuntu_deb_platform['rolling'],
'DISTRO_ARM_STATUS_SUFFIX': distro_arm_status_suffix.get('rolling', 'unv8'),
'REPOS_FILE_BRANCH': 'rolling',
'PRODUCT': 'ROS 2',
}

html_favicon = 'favicon.ico'
Expand All @@ -208,7 +229,7 @@
html_sourcelink_suffix = ''

# Relative to html_static_path
html_css_files = ['custom.css', 'adopters.css']
html_css_files = ['custom.css', 'adopters.css', 'pagefind-docsearch.css']
html_js_files = [
('vendor/pako.min.js', {'defer': ''}),
('vendor/js-yaml.min.js', {'defer': ''}),
Expand Down
70 changes: 70 additions & 0 deletions plugins/meta_util.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# Copyright 2026 Open Robotics — shared helpers for ``.. meta::`` / Pagefind
"""
Collect every ``.. meta::`` field from the doctree, sanitize keys, and expand
``{MACRO}`` placeholders using the Sphinx ``macros`` config (longest keys first).

Sphinx / the HTML theme may also emit plain ``<meta>`` tags for the same fields.
The Pagefind extension emits additional tags with ``data-pagefind-filter`` and may
split comma-separated values into multiple tags for faceted search.
"""

from __future__ import annotations

import re
from typing import Dict, List, Optional

from docutils import nodes

# HTML ``<meta name="...">`` names should be conservative; allow common patterns.
_META_NAME_RE = re.compile(r'^[A-Za-z0-9_.:-]+$')


def sanitize_meta_key(raw: str) -> Optional[str]:
s = str(raw).strip()
if not s or not _META_NAME_RE.match(s):
return None
return s


def all_doctree_meta(doctree: Optional[nodes.document]) -> Dict[str, str]:
"""Return last-wins mapping of every ``nodes.meta`` ``name``/``property`` → ``content``."""
if doctree is None:
return {}

out: Dict[str, str] = {}
for meta in doctree.findall(nodes.meta):
if meta.get('http-equiv'):
continue
content = meta.get('content')
if not content:
continue
key: Optional[str] = None
name = meta.get('name')
if name:
key = sanitize_meta_key(str(name))
else:
prop = meta.get('property')
if prop:
key = sanitize_meta_key(str(prop))
if not key:
continue
out[key] = str(content).strip()
return out


def expand_meta_macros(text: str, macros: Dict[str, str]) -> str:
"""Expand ``{KEY}`` placeholders; longer macro names first to avoid partial matches."""
result = text
for key, value in sorted(macros.items(), key=lambda kv: len(kv[0]), reverse=True):
result = result.replace(f'{{{key}}}', value)
return result


def expand_all_meta_values(meta: Dict[str, str], macros: Dict[str, str]) -> Dict[str, str]:
"""Apply ``expand_meta_macros`` to every meta value."""
return {k: expand_meta_macros(v, macros) for k, v in meta.items()}


def split_meta_values(value: str) -> List[str]:
"""Return comma-separated metadata values as individual Pagefind values."""
return [part.strip() for part in value.split(',') if part.strip()]
Loading
Loading