mkdocs-llms-source 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ """MkDocs plugin to generate /llms.txt for LLM-friendly documentation."""
2
+
3
+ try:
4
+ from mkdocs_llms_source._version import __version__
5
+ except ImportError: # pragma: no cover
6
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1,34 @@
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = [
5
+ "__version__",
6
+ "__version_tuple__",
7
+ "version",
8
+ "version_tuple",
9
+ "__commit_id__",
10
+ "commit_id",
11
+ ]
12
+
13
+ TYPE_CHECKING = False
14
+ if TYPE_CHECKING:
15
+ from typing import Tuple
16
+ from typing import Union
17
+
18
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
19
+ COMMIT_ID = Union[str, None]
20
+ else:
21
+ VERSION_TUPLE = object
22
+ COMMIT_ID = object
23
+
24
+ version: str
25
+ __version__: str
26
+ __version_tuple__: VERSION_TUPLE
27
+ version_tuple: VERSION_TUPLE
28
+ commit_id: COMMIT_ID
29
+ __commit_id__: COMMIT_ID
30
+
31
+ __version__ = version = '0.1.0'
32
+ __version_tuple__ = version_tuple = (0, 1, 0)
33
+
34
+ __commit_id__ = commit_id = None
@@ -0,0 +1,196 @@
1
+ """MkDocs plugin to generate /llms.txt, /llms-full.txt, and per-page .md files."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path, PurePosixPath
8
+
9
+ from mkdocs.config import config_options
10
+ from mkdocs.config.defaults import MkDocsConfig
11
+ from mkdocs.plugins import BasePlugin
12
+ from mkdocs.structure.files import Files
13
+ from mkdocs.structure.nav import Navigation, Section
14
+ from mkdocs.structure.pages import Page
15
+
16
+ log = logging.getLogger("mkdocs.plugins.llms_source")
17
+
18
+
19
+ @dataclass
20
+ class PageInfo:
21
+ """Metadata about a single documentation page."""
22
+
23
+ title: str
24
+ src_path: str
25
+ markdown: str = ""
26
+ description: str = ""
27
+
28
+ def md_url(self, base_url: str) -> str:
29
+ """Return the absolute URL to the .md version of this page."""
30
+ return f"{base_url}/{self.src_path}"
31
+
32
+
33
+ @dataclass
34
+ class SectionInfo:
35
+ """A section in the llms.txt output (maps to an H2 heading)."""
36
+
37
+ title: str
38
+ pages: list[PageInfo] = field(default_factory=list)
39
+
40
+
41
+ class LlmsTxtPlugin(BasePlugin):
42
+ """Generate /llms.txt, /llms-full.txt, and per-page .md files."""
43
+
44
+ config_scheme = (
45
+ ("full_output", config_options.Type(bool, default=True)),
46
+ ("markdown_urls", config_options.Type(bool, default=True)),
47
+ ("description", config_options.Type(str, default="")),
48
+ )
49
+
50
+ def __init__(self) -> None:
51
+ super().__init__()
52
+ self._pages: dict[str, str] = {} # src_path -> markdown content
53
+ self._sections: list[SectionInfo] = []
54
+ self._all_page_paths: set[str] = set()
55
+
56
+ def on_config(self, config: MkDocsConfig) -> MkDocsConfig:
57
+ """Validate configuration."""
58
+ if not config.get("site_url"):
59
+ log.warning("llms-source: site_url is not set — llms.txt will use relative URLs.")
60
+ return config
61
+
62
+ def on_files(self, files: Files, config: MkDocsConfig) -> Files:
63
+ """Track all documentation source files."""
64
+ for f in files.documentation_pages():
65
+ self._all_page_paths.add(f.src_path)
66
+ return files
67
+
68
+ def on_nav(self, nav: Navigation, config: MkDocsConfig, files: Files) -> Navigation:
69
+ """Walk the nav tree to auto-derive llms.txt sections."""
70
+ self._sections = []
71
+ self._walk_nav(nav.items)
72
+ return nav
73
+
74
+ def on_page_markdown(self, markdown: str, page: Page, config: MkDocsConfig, files: Files) -> str:
75
+ """Capture raw markdown for each page."""
76
+ self._pages[page.file.src_path] = markdown
77
+ return markdown
78
+
79
+ def on_post_build(self, config: MkDocsConfig) -> None:
80
+ """Generate llms.txt, llms-full.txt, and copy .md files to site output."""
81
+ site_dir = Path(config["site_dir"])
82
+ site_url = (config.get("site_url") or "").rstrip("/")
83
+ site_name = config.get("site_name", "Documentation")
84
+ site_desc = self.config.get("description") or config.get("site_description", "")
85
+
86
+ # Populate page markdown content into section structures
87
+ for section in self._sections:
88
+ for page_info in section.pages:
89
+ page_info.markdown = self._pages.get(page_info.src_path, "")
90
+
91
+ # Write llms.txt
92
+ llms_txt = self._build_llms_txt(site_name, site_desc, site_url)
93
+ (site_dir / "llms.txt").write_text(llms_txt, encoding="utf-8")
94
+ log.info("llms-source: Generated llms.txt")
95
+
96
+ # Write llms-full.txt
97
+ if self.config.get("full_output", True):
98
+ llms_full = self._build_llms_full(site_name, site_desc)
99
+ (site_dir / "llms-full.txt").write_text(llms_full, encoding="utf-8")
100
+ log.info("llms-source: Generated llms-full.txt")
101
+
102
+ # Copy per-page .md files
103
+ if self.config.get("markdown_urls", True):
104
+ self._copy_md_files(config, site_dir)
105
+
106
+ # ── Nav walking ──────────────────────────────────────────────
107
+
108
+ def _walk_nav(self, items: list, parent_title: str | None = None) -> None:
109
+ """Recursively walk nav items to build sections."""
110
+ for item in items:
111
+ if isinstance(item, Section):
112
+ section = SectionInfo(title=item.title)
113
+ self._sections.append(section)
114
+ self._collect_pages(item.children, section)
115
+ elif isinstance(item, Page):
116
+ # Top-level page (not inside a section)
117
+ title = self._page_title(item)
118
+ if parent_title is None:
119
+ # Create an implicit section for top-level pages
120
+ section = SectionInfo(title=title)
121
+ section.pages.append(
122
+ PageInfo(title=title, src_path=item.file.src_path)
123
+ )
124
+ self._sections.append(section)
125
+ # Skip Link items (external URLs)
126
+
127
+ def _collect_pages(self, items: list, section: SectionInfo) -> None:
128
+ """Collect pages from a nav section, including nested subsections."""
129
+ for item in items:
130
+ if isinstance(item, Page):
131
+ title = self._page_title(item)
132
+ section.pages.append(
133
+ PageInfo(title=title, src_path=item.file.src_path)
134
+ )
135
+ elif isinstance(item, Section):
136
+ # Flatten nested sections into the parent section
137
+ self._collect_pages(item.children, section)
138
+ # Skip Link items (external URLs)
139
+
140
+ @staticmethod
141
+ def _page_title(page: Page) -> str:
142
+ """Get the best available title for a page."""
143
+ if page.title:
144
+ return page.title
145
+ # Fallback: derive from filename
146
+ return PurePosixPath(page.file.src_path).stem.replace("-", " ").replace("_", " ").title()
147
+
148
+ # ── Output generation ────────────────────────────────────────
149
+
150
+ def _build_llms_txt(self, site_name: str, site_desc: str, site_url: str) -> str:
151
+ """Build the llms.txt index content."""
152
+ lines: list[str] = []
153
+ lines.append(f"# {site_name}\n")
154
+
155
+ if site_desc:
156
+ lines.append(f"> {site_desc}\n")
157
+
158
+ for section in self._sections:
159
+ lines.append(f"## {section.title}\n")
160
+ for page in section.pages:
161
+ url = page.md_url(site_url) if site_url else page.src_path
162
+ desc_part = f": {page.description}" if page.description else ""
163
+ lines.append(f"- [{page.title}]({url}){desc_part}")
164
+ lines.append("")
165
+
166
+ return "\n".join(lines).rstrip() + "\n"
167
+
168
+ def _build_llms_full(self, site_name: str, site_desc: str) -> str:
169
+ """Build the llms-full.txt with all page content concatenated."""
170
+ lines: list[str] = []
171
+ lines.append(f"# {site_name}\n")
172
+
173
+ if site_desc:
174
+ lines.append(f"> {site_desc}\n")
175
+
176
+ for section in self._sections:
177
+ lines.append(f"## {section.title}\n")
178
+ for page in section.pages:
179
+ if page.markdown:
180
+ lines.append(f"### {page.title}\n")
181
+ lines.append(page.markdown.strip())
182
+ lines.append("")
183
+
184
+ return "\n".join(lines).rstrip() + "\n"
185
+
186
+ def _copy_md_files(self, config: MkDocsConfig, site_dir: Path) -> None:
187
+ """Copy source .md files into the site output directory."""
188
+ copied = 0
189
+
190
+ for src_path, markdown in self._pages.items():
191
+ dest = site_dir / src_path
192
+ dest.parent.mkdir(parents=True, exist_ok=True)
193
+ dest.write_text(markdown, encoding="utf-8")
194
+ copied += 1
195
+
196
+ log.info("llms-source: Copied %d .md files to site output", copied)
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: mkdocs-llms-source
3
+ Version: 0.1.0
4
+ Summary: MkDocs plugin to generate /llms.txt files for LLM-friendly documentation
5
+ Project-URL: Homepage, https://github.com/TimChild/mkdocs-llms-source
6
+ Project-URL: Documentation, https://github.com/TimChild/mkdocs-llms-source
7
+ Project-URL: Repository, https://github.com/TimChild/mkdocs-llms-source
8
+ Project-URL: Issues, https://github.com/TimChild/mkdocs-llms-source/issues
9
+ Author: Tim Child
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: ai,documentation,llms,llmstxt,mkdocs
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Documentation
22
+ Classifier: Topic :: Software Development :: Documentation
23
+ Requires-Python: >=3.10
24
+ Requires-Dist: mkdocs>=1.5
25
+ Provides-Extra: dev
26
+ Requires-Dist: mkdocs-material>=9.0; extra == 'dev'
27
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: ruff>=0.4; extra == 'dev'
30
+ Description-Content-Type: text/markdown
31
+
32
+ # mkdocs-llms-source
33
+
34
+ [![CI](https://github.com/TimChild/mkdocs-llms-source/actions/workflows/ci.yml/badge.svg)](https://github.com/TimChild/mkdocs-llms-source/actions/workflows/ci.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/mkdocs-llms-source)](https://pypi.org/project/mkdocs-llms-source/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/mkdocs-llms-source)](https://pypi.org/project/mkdocs-llms-source/)
37
+
38
+ MkDocs plugin to generate [`/llms.txt`](https://llmstxt.org/) files for LLM-friendly documentation.
39
+
40
+ ## What It Does
41
+
42
+ Generates three outputs from your MkDocs site:
43
+
44
+ 1. **`/llms.txt`** — A curated index following the [llmstxt.org spec](https://llmstxt.org/) with links to per-page markdown files
45
+ 2. **`/llms-full.txt`** — All documentation concatenated into a single file (for stuffing into LLM context windows)
46
+ 3. **Per-page `.md` files** — Raw markdown accessible at the same URL path as HTML pages
47
+
48
+ ## Install
49
+
50
+ ```bash
51
+ uv add mkdocs-llms-source
52
+ ```
53
+
54
+ ## Usage
55
+
56
+ Add to your `mkdocs.yml`:
57
+
58
+ ```yaml
59
+ site_name: My Project
60
+ site_url: https://docs.example.com
61
+ site_description: Documentation for My Project
62
+
63
+ plugins:
64
+ - search
65
+ - llms-source
66
+ ```
67
+
68
+ Build your site:
69
+
70
+ ```bash
71
+ mkdocs build
72
+ ```
73
+
74
+ The plugin auto-derives the llms.txt section structure from your MkDocs `nav` — zero extra configuration needed.
75
+
76
+ ## Configuration
77
+
78
+ ```yaml
79
+ plugins:
80
+ - llms-source:
81
+ full_output: true # Generate llms-full.txt (default: true)
82
+ markdown_urls: true # Copy .md source files to output (default: true)
83
+ description: "Override description for llms.txt header"
84
+ ```
85
+
86
+ ## How It Works
87
+
88
+ **Source-first approach**: The plugin uses your original markdown source files directly — no HTML-to-Markdown conversion. This is simpler, more reliable, and preserves your intended formatting.
89
+
90
+ The llms.txt sections are automatically derived from your MkDocs `nav` configuration, so top-level nav items become H2 sections in the output.
91
+
92
+ ## Example Output
93
+
94
+ Given this nav:
95
+
96
+ ```yaml
97
+ nav:
98
+ - Home: index.md
99
+ - Guides:
100
+ - Getting Started: guides/setup.md
101
+ ```
102
+
103
+ The generated `/llms.txt`:
104
+
105
+ ```markdown
106
+ # My Project
107
+
108
+ > Documentation for My Project
109
+
110
+ ## Home
111
+
112
+ - [My Project](https://docs.example.com/index.md)
113
+
114
+ ## Guides
115
+
116
+ - [Getting Started](https://docs.example.com/guides/setup.md)
117
+ ```
118
+
119
+ ## License
120
+
121
+ MIT
@@ -0,0 +1,8 @@
1
+ mkdocs_llms_source/__init__.py,sha256=fs2s5gb88ambMksgQzvbIzD0MK0AogfwGjmILmXCfjQ,210
2
+ mkdocs_llms_source/_version.py,sha256=5jwwVncvCiTnhOedfkzzxmxsggwmTBORdFL_4wq0ZeY,704
3
+ mkdocs_llms_source/plugin.py,sha256=NDR_b-XdLInU0txJGU-w1TzaWabLSbp53KVUE2x0rOo,7798
4
+ mkdocs_llms_source-0.1.0.dist-info/METADATA,sha256=VN6cRjMYs9QZWh-tD6bm4pPdAHxbVDA3srPj4nV8TLA,3647
5
+ mkdocs_llms_source-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
6
+ mkdocs_llms_source-0.1.0.dist-info/entry_points.txt,sha256=lWLByqlEUegBdhGQEh1ekjCKnHIS_rd_2vKL1p5B8qU,71
7
+ mkdocs_llms_source-0.1.0.dist-info/licenses/LICENSE,sha256=dM-_y73WaASlMKc125O9rlz2AXKO3tOtBhU1d2NX4ao,1066
8
+ mkdocs_llms_source-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [mkdocs.plugins]
2
+ llms-source = mkdocs_llms_source.plugin:LlmsTxtPlugin
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tim Child
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.