From ef4158010a56f0f3e246c70302a97e1e8044c721 Mon Sep 17 00:00:00 2001 From: Musa Date: Tue, 23 Dec 2025 12:09:25 -0800 Subject: [PATCH] feat(docs): include llms.txt --- docs/source/_ext/llms_txt.py | 93 ++++++++++++++++++++++++++++++ docs/source/conf.py | 7 +++ docs/source/index.rst | 1 + docs/source/resources/llms_txt.rst | 6 ++ 4 files changed, 107 insertions(+) create mode 100644 docs/source/_ext/llms_txt.py create mode 100644 docs/source/resources/llms_txt.rst diff --git a/docs/source/_ext/llms_txt.py b/docs/source/_ext/llms_txt.py new file mode 100644 index 00000000..cfafadf4 --- /dev/null +++ b/docs/source/_ext/llms_txt.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime, timezone +from pathlib import Path +from typing import Iterable + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + # Only for type-checkers; Sphinx is only required in the docs build environment. + from sphinx.application import Sphinx # type: ignore[import-not-found] + + +@dataclass(frozen=True) +class LlmsTxtDoc: + docname: str + title: str + text: str + + +def _iter_docs(app: Sphinx) -> Iterable[LlmsTxtDoc]: + env = app.env + + # Sphinx internal pages that shouldn't be included. + excluded = {"genindex", "search"} + + for docname in sorted(d for d in env.found_docs if d not in excluded): + title_node = env.titles.get(docname) + title = title_node.astext().strip() if title_node else docname + + doctree = env.get_doctree(docname) + text = doctree.astext().strip() + + yield LlmsTxtDoc(docname=docname, title=title, text=text) + + +def _render_llms_txt(app: Sphinx) -> str: + now = datetime.now(timezone.utc).isoformat() + + project = str(getattr(app.config, "project", "")).strip() + release = str(getattr(app.config, "release", "")).strip() + header = f"{project} {release}".strip() or "Documentation" + + docs = list(_iter_docs(app)) + + lines: list[str] = [] + lines.append(header) + lines.append("llms.txt (auto-generated)") + lines.append(f"Generated (UTC): {now}") + lines.append("") + lines.append("Table of contents") + for d in docs: + lines.append(f"- {d.title} ({d.docname})") + lines.append("") + + for d in docs: + lines.append(d.title) + lines.append("-" * max(3, len(d.title))) + lines.append(f"Doc: {d.docname}") + lines.append("") + if d.text: + lines.append(d.text) + else: + lines.append("(empty)") + lines.append("") + lines.append("---") + lines.append("") + + return "\n".join(lines).replace("\r\n", "\n").strip() + "\n" + + +def _on_build_finished(app: Sphinx, exception: Exception | None) -> None: + if exception is not None: + return + + # Only generate for HTML-like builders where app.outdir is a website root. + if getattr(app.builder, "format", None) != "html": + return + + # Per repo convention, place generated artifacts under an `includes/` folder. + out_path = Path(app.outdir) / "includes" / "llms.txt" + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(_render_llms_txt(app), encoding="utf-8") + + +def setup(app: Sphinx) -> dict[str, object]: + app.connect("build-finished", _on_build_finished) + return { + "version": "0.1.0", + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/docs/source/conf.py b/docs/source/conf.py index 20e1f588..8457bbe6 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -5,6 +5,8 @@ # -- Project information ----------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information +import os +import sys from dataclasses import asdict from sphinx.application import Sphinx @@ -34,6 +36,8 @@ extensions = [ "sphinx.ext.viewcode", "sphinx_sitemap", "sphinx_design", + # Local extensions + "llms_txt", ] # Paths that contain templates, relative to this directory. @@ -43,6 +47,9 @@ templates_path = ["_templates"] # to ignore when looking for source files. exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] +# Allow importing extensions from docs/source/_ext (robust to current working directory) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext"))) + # -- Options for HTML output ------------------------------------------------- html_theme = "sphinxawesome_theme" # You can change the theme to 'sphinx_rtd_theme' or another of your choice. diff --git a/docs/source/index.rst b/docs/source/index.rst index a4825ca1..158891f4 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -61,3 +61,4 @@ Built by contributors to the widely adopted `Envoy Proxy `_