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.
- mkdocs_llms_source/__init__.py +6 -0
- mkdocs_llms_source/_version.py +34 -0
- mkdocs_llms_source/plugin.py +196 -0
- mkdocs_llms_source-0.1.0.dist-info/METADATA +121 -0
- mkdocs_llms_source-0.1.0.dist-info/RECORD +8 -0
- mkdocs_llms_source-0.1.0.dist-info/WHEEL +4 -0
- mkdocs_llms_source-0.1.0.dist-info/entry_points.txt +2 -0
- mkdocs_llms_source-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
[](https://github.com/TimChild/mkdocs-llms-source/actions/workflows/ci.yml)
|
|
35
|
+
[](https://pypi.org/project/mkdocs-llms-source/)
|
|
36
|
+
[](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,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.
|