mkdocs-ultralytics-plugin 0.2.4__tar.gz → 0.2.5__tar.gz
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_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/PKG-INFO +7 -5
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/README.md +6 -4
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/PKG-INFO +7 -5
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/__init__.py +1 -1
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/main.py +25 -1
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/postprocess.py +117 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/processor.py +16 -12
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/utils.py +45 -6
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/LICENSE +0 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/SOURCES.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/dependency_links.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/entry_points.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/requires.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/top_level.txt +0 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/pyproject.toml +0 -0
- {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs-ultralytics-plugin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: An MkDocs plugin that provides Ultralytics Docs customizations at https://docs.ultralytics.com.
|
|
5
5
|
Author-email: Glenn Jocher <hello@ultralytics.com>
|
|
6
6
|
Maintainer-email: Ultralytics <hello@ultralytics.com>
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
# 🚀 MkDocs Ultralytics Plugin
|
|
47
47
|
|
|
48
|
-
Welcome to the MkDocs Ultralytics Plugin! 📄 This powerful tool enhances your [MkDocs](https://www.mkdocs.org/), [Zensical](https://zensical.
|
|
48
|
+
Welcome to the MkDocs Ultralytics Plugin! 📄 This powerful tool enhances your [MkDocs](https://www.mkdocs.org/), [Zensical](https://zensical.org/), or any static site documentation with advanced Search Engine Optimization (SEO) features, interactive social elements, and structured data support. It automates the generation of essential meta tags, incorporates social sharing capabilities, and adds [JSON-LD](https://json-ld.org/) structured data to elevate user engagement and improve your documentation's visibility on the web.
|
|
49
49
|
|
|
50
50
|
**Two modes available:**
|
|
51
51
|
|
|
@@ -64,12 +64,13 @@ This tool seamlessly integrates valuable features into your documentation site:
|
|
|
64
64
|
|
|
65
65
|
- **Meta Tag Generation**: Automatically creates meta description and image tags using the first paragraph and image found on each page, crucial for SEO and social previews.
|
|
66
66
|
- **Keyword Customization**: Allows you to define specific meta keywords directly within your Markdown front matter for targeted SEO.
|
|
67
|
-
- **Social Media Optimization**: Generates [Open Graph](https://ogp.me/) and [Twitter Card](https://
|
|
67
|
+
- **Social Media Optimization**: Generates [Open Graph](https://ogp.me/) and [Twitter Card](https://docs.x.com/overview) meta tags to ensure your content looks great when shared on social platforms.
|
|
68
68
|
- **Simple Sharing**: Inserts convenient share buttons for Twitter and LinkedIn at the end of your content, encouraging readers to share.
|
|
69
69
|
- **Git Insights**: Gathers and displays [Git](https://git-scm.com/) commit information, including update dates and authors, directly within the page footer for transparency.
|
|
70
70
|
- **JSON-LD Support**: Adds structured data in JSON-LD format, helping search engines understand your content better and potentially enabling rich results.
|
|
71
71
|
- **FAQ Parsing**: Automatically parses FAQ sections (if present) and includes them in the structured data for enhanced search visibility.
|
|
72
72
|
- **Copy for LLM**: Adds a button to copy page content in Markdown format, optimized for sharing with AI assistants.
|
|
73
|
+
- **LLMs.txt Generation**: Generates an `llms.txt` index after builds for LLM-friendly site discovery.
|
|
73
74
|
- **Customizable Styling**: Includes optional inline CSS to maintain consistent styling across your documentation, aligning with themes like [MkDocs Material](https://squidfunk.github.io/mkdocs-material/).
|
|
74
75
|
|
|
75
76
|
## 🛠️ Installation
|
|
@@ -177,6 +178,7 @@ Both modes support the same configuration options:
|
|
|
177
178
|
| `add_json_ld` | bool | `False` | Add JSON-LD structured data |
|
|
178
179
|
| `add_css` | bool | `True` | Include inline CSS styles |
|
|
179
180
|
| `add_copy_llm` | bool | `True` | Add "Copy for LLM" button |
|
|
181
|
+
| `add_llms_txt` | bool | `True` | Generate an `llms.txt` index |
|
|
180
182
|
|
|
181
183
|
## 🧩 How It Works
|
|
182
184
|
|
|
@@ -244,7 +246,7 @@ Please see our [Contributing Guide](https://docs.ultralytics.com/help/contributi
|
|
|
244
246
|
|
|
245
247
|
Ultralytics provides two licensing options:
|
|
246
248
|
|
|
247
|
-
- **AGPL-3.0 License**: Ideal for students, researchers, and enthusiasts, this [OSI-approved](https://opensource.org/license/agpl-
|
|
249
|
+
- **AGPL-3.0 License**: Ideal for students, researchers, and enthusiasts, this [OSI-approved](https://opensource.org/license/agpl-3.0) license promotes open collaboration and knowledge sharing. See the [LICENSE](https://github.com/ultralytics/mkdocs/blob/main/LICENSE) file for details.
|
|
248
250
|
- **Enterprise License**: Designed for commercial applications, this license allows seamless integration of Ultralytics software into commercial products, bypassing AGPL-3.0 requirements. Visit [Ultralytics Licensing](https://www.ultralytics.com/license) for details.
|
|
249
251
|
|
|
250
252
|
## ✉️ Connect with Us
|
|
@@ -259,7 +261,7 @@ Encountered a bug or have an idea? Visit [GitHub Issues](https://github.com/ultr
|
|
|
259
261
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
260
262
|
<a href="https://twitter.com/ultralytics"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-twitter.png" width="3%" alt="Ultralytics Twitter"></a>
|
|
261
263
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
262
|
-
<a href="https://youtube.com/ultralytics?sub_confirmation=1"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-youtube.png" width="3%" alt="Ultralytics YouTube"></a>
|
|
264
|
+
<a href="https://www.youtube.com/ultralytics?sub_confirmation=1"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-youtube.png" width="3%" alt="Ultralytics YouTube"></a>
|
|
263
265
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
264
266
|
<a href="https://www.tiktok.com/@ultralytics"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-tiktok.png" width="3%" alt="Ultralytics TikTok"></a>
|
|
265
267
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# 🚀 MkDocs Ultralytics Plugin
|
|
4
4
|
|
|
5
|
-
Welcome to the MkDocs Ultralytics Plugin! 📄 This powerful tool enhances your [MkDocs](https://www.mkdocs.org/), [Zensical](https://zensical.
|
|
5
|
+
Welcome to the MkDocs Ultralytics Plugin! 📄 This powerful tool enhances your [MkDocs](https://www.mkdocs.org/), [Zensical](https://zensical.org/), or any static site documentation with advanced Search Engine Optimization (SEO) features, interactive social elements, and structured data support. It automates the generation of essential meta tags, incorporates social sharing capabilities, and adds [JSON-LD](https://json-ld.org/) structured data to elevate user engagement and improve your documentation's visibility on the web.
|
|
6
6
|
|
|
7
7
|
**Two modes available:**
|
|
8
8
|
|
|
@@ -21,12 +21,13 @@ This tool seamlessly integrates valuable features into your documentation site:
|
|
|
21
21
|
|
|
22
22
|
- **Meta Tag Generation**: Automatically creates meta description and image tags using the first paragraph and image found on each page, crucial for SEO and social previews.
|
|
23
23
|
- **Keyword Customization**: Allows you to define specific meta keywords directly within your Markdown front matter for targeted SEO.
|
|
24
|
-
- **Social Media Optimization**: Generates [Open Graph](https://ogp.me/) and [Twitter Card](https://
|
|
24
|
+
- **Social Media Optimization**: Generates [Open Graph](https://ogp.me/) and [Twitter Card](https://docs.x.com/overview) meta tags to ensure your content looks great when shared on social platforms.
|
|
25
25
|
- **Simple Sharing**: Inserts convenient share buttons for Twitter and LinkedIn at the end of your content, encouraging readers to share.
|
|
26
26
|
- **Git Insights**: Gathers and displays [Git](https://git-scm.com/) commit information, including update dates and authors, directly within the page footer for transparency.
|
|
27
27
|
- **JSON-LD Support**: Adds structured data in JSON-LD format, helping search engines understand your content better and potentially enabling rich results.
|
|
28
28
|
- **FAQ Parsing**: Automatically parses FAQ sections (if present) and includes them in the structured data for enhanced search visibility.
|
|
29
29
|
- **Copy for LLM**: Adds a button to copy page content in Markdown format, optimized for sharing with AI assistants.
|
|
30
|
+
- **LLMs.txt Generation**: Generates an `llms.txt` index after builds for LLM-friendly site discovery.
|
|
30
31
|
- **Customizable Styling**: Includes optional inline CSS to maintain consistent styling across your documentation, aligning with themes like [MkDocs Material](https://squidfunk.github.io/mkdocs-material/).
|
|
31
32
|
|
|
32
33
|
## 🛠️ Installation
|
|
@@ -134,6 +135,7 @@ Both modes support the same configuration options:
|
|
|
134
135
|
| `add_json_ld` | bool | `False` | Add JSON-LD structured data |
|
|
135
136
|
| `add_css` | bool | `True` | Include inline CSS styles |
|
|
136
137
|
| `add_copy_llm` | bool | `True` | Add "Copy for LLM" button |
|
|
138
|
+
| `add_llms_txt` | bool | `True` | Generate an `llms.txt` index |
|
|
137
139
|
|
|
138
140
|
## 🧩 How It Works
|
|
139
141
|
|
|
@@ -201,7 +203,7 @@ Please see our [Contributing Guide](https://docs.ultralytics.com/help/contributi
|
|
|
201
203
|
|
|
202
204
|
Ultralytics provides two licensing options:
|
|
203
205
|
|
|
204
|
-
- **AGPL-3.0 License**: Ideal for students, researchers, and enthusiasts, this [OSI-approved](https://opensource.org/license/agpl-
|
|
206
|
+
- **AGPL-3.0 License**: Ideal for students, researchers, and enthusiasts, this [OSI-approved](https://opensource.org/license/agpl-3.0) license promotes open collaboration and knowledge sharing. See the [LICENSE](https://github.com/ultralytics/mkdocs/blob/main/LICENSE) file for details.
|
|
205
207
|
- **Enterprise License**: Designed for commercial applications, this license allows seamless integration of Ultralytics software into commercial products, bypassing AGPL-3.0 requirements. Visit [Ultralytics Licensing](https://www.ultralytics.com/license) for details.
|
|
206
208
|
|
|
207
209
|
## ✉️ Connect with Us
|
|
@@ -216,7 +218,7 @@ Encountered a bug or have an idea? Visit [GitHub Issues](https://github.com/ultr
|
|
|
216
218
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
217
219
|
<a href="https://twitter.com/ultralytics"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-twitter.png" width="3%" alt="Ultralytics Twitter"></a>
|
|
218
220
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
219
|
-
<a href="https://youtube.com/ultralytics?sub_confirmation=1"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-youtube.png" width="3%" alt="Ultralytics YouTube"></a>
|
|
221
|
+
<a href="https://www.youtube.com/ultralytics?sub_confirmation=1"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-youtube.png" width="3%" alt="Ultralytics YouTube"></a>
|
|
220
222
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
221
223
|
<a href="https://www.tiktok.com/@ultralytics"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-tiktok.png" width="3%" alt="Ultralytics TikTok"></a>
|
|
222
224
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mkdocs-ultralytics-plugin
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.5
|
|
4
4
|
Summary: An MkDocs plugin that provides Ultralytics Docs customizations at https://docs.ultralytics.com.
|
|
5
5
|
Author-email: Glenn Jocher <hello@ultralytics.com>
|
|
6
6
|
Maintainer-email: Ultralytics <hello@ultralytics.com>
|
|
@@ -45,7 +45,7 @@ Dynamic: license-file
|
|
|
45
45
|
|
|
46
46
|
# 🚀 MkDocs Ultralytics Plugin
|
|
47
47
|
|
|
48
|
-
Welcome to the MkDocs Ultralytics Plugin! 📄 This powerful tool enhances your [MkDocs](https://www.mkdocs.org/), [Zensical](https://zensical.
|
|
48
|
+
Welcome to the MkDocs Ultralytics Plugin! 📄 This powerful tool enhances your [MkDocs](https://www.mkdocs.org/), [Zensical](https://zensical.org/), or any static site documentation with advanced Search Engine Optimization (SEO) features, interactive social elements, and structured data support. It automates the generation of essential meta tags, incorporates social sharing capabilities, and adds [JSON-LD](https://json-ld.org/) structured data to elevate user engagement and improve your documentation's visibility on the web.
|
|
49
49
|
|
|
50
50
|
**Two modes available:**
|
|
51
51
|
|
|
@@ -64,12 +64,13 @@ This tool seamlessly integrates valuable features into your documentation site:
|
|
|
64
64
|
|
|
65
65
|
- **Meta Tag Generation**: Automatically creates meta description and image tags using the first paragraph and image found on each page, crucial for SEO and social previews.
|
|
66
66
|
- **Keyword Customization**: Allows you to define specific meta keywords directly within your Markdown front matter for targeted SEO.
|
|
67
|
-
- **Social Media Optimization**: Generates [Open Graph](https://ogp.me/) and [Twitter Card](https://
|
|
67
|
+
- **Social Media Optimization**: Generates [Open Graph](https://ogp.me/) and [Twitter Card](https://docs.x.com/overview) meta tags to ensure your content looks great when shared on social platforms.
|
|
68
68
|
- **Simple Sharing**: Inserts convenient share buttons for Twitter and LinkedIn at the end of your content, encouraging readers to share.
|
|
69
69
|
- **Git Insights**: Gathers and displays [Git](https://git-scm.com/) commit information, including update dates and authors, directly within the page footer for transparency.
|
|
70
70
|
- **JSON-LD Support**: Adds structured data in JSON-LD format, helping search engines understand your content better and potentially enabling rich results.
|
|
71
71
|
- **FAQ Parsing**: Automatically parses FAQ sections (if present) and includes them in the structured data for enhanced search visibility.
|
|
72
72
|
- **Copy for LLM**: Adds a button to copy page content in Markdown format, optimized for sharing with AI assistants.
|
|
73
|
+
- **LLMs.txt Generation**: Generates an `llms.txt` index after builds for LLM-friendly site discovery.
|
|
73
74
|
- **Customizable Styling**: Includes optional inline CSS to maintain consistent styling across your documentation, aligning with themes like [MkDocs Material](https://squidfunk.github.io/mkdocs-material/).
|
|
74
75
|
|
|
75
76
|
## 🛠️ Installation
|
|
@@ -177,6 +178,7 @@ Both modes support the same configuration options:
|
|
|
177
178
|
| `add_json_ld` | bool | `False` | Add JSON-LD structured data |
|
|
178
179
|
| `add_css` | bool | `True` | Include inline CSS styles |
|
|
179
180
|
| `add_copy_llm` | bool | `True` | Add "Copy for LLM" button |
|
|
181
|
+
| `add_llms_txt` | bool | `True` | Generate an `llms.txt` index |
|
|
180
182
|
|
|
181
183
|
## 🧩 How It Works
|
|
182
184
|
|
|
@@ -244,7 +246,7 @@ Please see our [Contributing Guide](https://docs.ultralytics.com/help/contributi
|
|
|
244
246
|
|
|
245
247
|
Ultralytics provides two licensing options:
|
|
246
248
|
|
|
247
|
-
- **AGPL-3.0 License**: Ideal for students, researchers, and enthusiasts, this [OSI-approved](https://opensource.org/license/agpl-
|
|
249
|
+
- **AGPL-3.0 License**: Ideal for students, researchers, and enthusiasts, this [OSI-approved](https://opensource.org/license/agpl-3.0) license promotes open collaboration and knowledge sharing. See the [LICENSE](https://github.com/ultralytics/mkdocs/blob/main/LICENSE) file for details.
|
|
248
250
|
- **Enterprise License**: Designed for commercial applications, this license allows seamless integration of Ultralytics software into commercial products, bypassing AGPL-3.0 requirements. Visit [Ultralytics Licensing](https://www.ultralytics.com/license) for details.
|
|
249
251
|
|
|
250
252
|
## ✉️ Connect with Us
|
|
@@ -259,7 +261,7 @@ Encountered a bug or have an idea? Visit [GitHub Issues](https://github.com/ultr
|
|
|
259
261
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
260
262
|
<a href="https://twitter.com/ultralytics"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-twitter.png" width="3%" alt="Ultralytics Twitter"></a>
|
|
261
263
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
262
|
-
<a href="https://youtube.com/ultralytics?sub_confirmation=1"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-youtube.png" width="3%" alt="Ultralytics YouTube"></a>
|
|
264
|
+
<a href="https://www.youtube.com/ultralytics?sub_confirmation=1"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-youtube.png" width="3%" alt="Ultralytics YouTube"></a>
|
|
263
265
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
264
266
|
<a href="https://www.tiktok.com/@ultralytics"><img src="https://github.com/ultralytics/assets/raw/main/social/logo-social-tiktok.png" width="3%" alt="Ultralytics TikTok"></a>
|
|
265
267
|
<img src="https://github.com/ultralytics/assets/raw/main/social/logo-transparent.png" width="3%" alt="space">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
|
|
2
|
+
"""MkDocs plugin entrypoint for Ultralytics documentation metadata."""
|
|
2
3
|
|
|
3
4
|
from __future__ import annotations
|
|
4
5
|
|
|
@@ -9,6 +10,7 @@ from mkdocs.plugins import BasePlugin
|
|
|
9
10
|
|
|
10
11
|
import plugin.processor as processor
|
|
11
12
|
from plugin.processor import process_html
|
|
13
|
+
from plugin.utils import resolve_all_authors
|
|
12
14
|
|
|
13
15
|
|
|
14
16
|
class MetaPlugin(BasePlugin):
|
|
@@ -27,9 +29,11 @@ class MetaPlugin(BasePlugin):
|
|
|
27
29
|
("add_json_ld", config_options.Type(bool, default=False)),
|
|
28
30
|
("add_css", config_options.Type(bool, default=True)),
|
|
29
31
|
("add_copy_llm", config_options.Type(bool, default=True)),
|
|
32
|
+
("add_llms_txt", config_options.Type(bool, default=True)),
|
|
30
33
|
)
|
|
31
34
|
|
|
32
35
|
def __init__(self):
|
|
36
|
+
"""Initialize cached repository metadata for page processing."""
|
|
33
37
|
super().__init__()
|
|
34
38
|
self.git_repo_url = None
|
|
35
39
|
self.git_data = None
|
|
@@ -43,6 +47,12 @@ class MetaPlugin(BasePlugin):
|
|
|
43
47
|
docs_dir = Path(config["docs_dir"])
|
|
44
48
|
md_files = [str(p) for p in docs_dir.rglob("*.md")] if docs_dir.exists() else []
|
|
45
49
|
self.git_repo_url, self.git_data = processor.build_git_map(md_files)
|
|
50
|
+
self.git_data = resolve_all_authors(
|
|
51
|
+
self.git_data,
|
|
52
|
+
default_author=self.config.get("default_author"),
|
|
53
|
+
repo_url=self.git_repo_url,
|
|
54
|
+
verbose=self.config.get("verbose", True),
|
|
55
|
+
)
|
|
46
56
|
return config
|
|
47
57
|
|
|
48
58
|
def on_post_page(self, output: str, page, config) -> str:
|
|
@@ -69,7 +79,6 @@ class MetaPlugin(BasePlugin):
|
|
|
69
79
|
git_data=self.git_data,
|
|
70
80
|
repo_url=self.git_repo_url,
|
|
71
81
|
default_image=self.config["default_image"],
|
|
72
|
-
default_author=self.config["default_author"],
|
|
73
82
|
keywords=keywords,
|
|
74
83
|
add_desc=self.config["add_desc"],
|
|
75
84
|
add_image=self.config["add_image"],
|
|
@@ -84,3 +93,18 @@ class MetaPlugin(BasePlugin):
|
|
|
84
93
|
if self.config["verbose"]:
|
|
85
94
|
print(f"ERROR - mkdocs-ultralytics-plugin: Failed to process {page.file.src_path}: {e}")
|
|
86
95
|
return output # Return original output on error
|
|
96
|
+
|
|
97
|
+
def on_post_build(self, config):
|
|
98
|
+
"""Generate llms.txt after build completes. Added for mkdocs build compatibility. Not needed for zensical build."""
|
|
99
|
+
if not self.config.get("enabled", True) or not self.config.get("add_llms_txt", True):
|
|
100
|
+
return
|
|
101
|
+
from plugin.postprocess import generate_llms_txt
|
|
102
|
+
|
|
103
|
+
generate_llms_txt(
|
|
104
|
+
site_dir=Path(config["site_dir"]),
|
|
105
|
+
docs_dir=Path(config["docs_dir"]),
|
|
106
|
+
site_url=config.get("site_url", ""),
|
|
107
|
+
site_name=config.get("site_name"),
|
|
108
|
+
site_description=config.get("site_description"),
|
|
109
|
+
nav=config.get("nav"),
|
|
110
|
+
)
|
|
@@ -134,6 +134,119 @@ def process_html_file(
|
|
|
134
134
|
return False
|
|
135
135
|
|
|
136
136
|
|
|
137
|
+
def generate_llms_txt(
|
|
138
|
+
site_dir: Path,
|
|
139
|
+
docs_dir: Path,
|
|
140
|
+
site_url: str,
|
|
141
|
+
site_name: str | None = None,
|
|
142
|
+
site_description: str | None = None,
|
|
143
|
+
nav: list | None = None,
|
|
144
|
+
) -> None:
|
|
145
|
+
"""Generate llms.txt file for LLM consumption."""
|
|
146
|
+
import yaml
|
|
147
|
+
|
|
148
|
+
# Fallback to reading mkdocs.yml if config values not provided (standalone postprocess mode)
|
|
149
|
+
if site_name is None or nav is None:
|
|
150
|
+
|
|
151
|
+
class _Loader(yaml.SafeLoader):
|
|
152
|
+
pass
|
|
153
|
+
|
|
154
|
+
_Loader.add_multi_constructor("", lambda loader, suffix, node: None)
|
|
155
|
+
|
|
156
|
+
mkdocs_yml = site_dir.parent / "mkdocs.yml"
|
|
157
|
+
if mkdocs_yml.exists():
|
|
158
|
+
config = yaml.load(mkdocs_yml.read_text(), Loader=_Loader) or {}
|
|
159
|
+
site_name = site_name or config.get("site_name", "Documentation")
|
|
160
|
+
site_description = site_description or config.get("site_description", "")
|
|
161
|
+
nav = nav or config.get("nav")
|
|
162
|
+
site_name = site_name or "Documentation"
|
|
163
|
+
site_description = site_description or ""
|
|
164
|
+
|
|
165
|
+
lines = [f"# {site_name}", f"> {site_description}"]
|
|
166
|
+
seen_urls: set[str] = set()
|
|
167
|
+
site_url = site_url.rstrip("/")
|
|
168
|
+
|
|
169
|
+
def get_description(md_path: Path) -> str:
|
|
170
|
+
"""Extract description from markdown frontmatter."""
|
|
171
|
+
try:
|
|
172
|
+
content = md_path.read_text()
|
|
173
|
+
if content.startswith("---"):
|
|
174
|
+
end = content.find("\n---\n", 3)
|
|
175
|
+
if end != -1:
|
|
176
|
+
fm = yaml.safe_load(content[4:end]) or {}
|
|
177
|
+
return fm.get("description", "")
|
|
178
|
+
except Exception:
|
|
179
|
+
pass
|
|
180
|
+
return ""
|
|
181
|
+
|
|
182
|
+
def md_to_url(md_path: str) -> str:
|
|
183
|
+
"""Convert markdown path to HTML URL."""
|
|
184
|
+
url = md_path.replace(".md", "/").replace("/index/", "/")
|
|
185
|
+
return f"{site_url}/{url}" if url != "index/" else f"{site_url}/"
|
|
186
|
+
|
|
187
|
+
if nav:
|
|
188
|
+
|
|
189
|
+
def process_items(items, indent=0):
|
|
190
|
+
"""Recursively process nav items with indentation (Vercel-style)."""
|
|
191
|
+
prefix = " " * indent + "- "
|
|
192
|
+
for item in items:
|
|
193
|
+
if isinstance(item, str):
|
|
194
|
+
md = docs_dir / item
|
|
195
|
+
if md.exists():
|
|
196
|
+
url = md_to_url(item)
|
|
197
|
+
if url in seen_urls:
|
|
198
|
+
continue
|
|
199
|
+
seen_urls.add(url)
|
|
200
|
+
desc = get_description(md)
|
|
201
|
+
# Use parent dir name for index.md, else filename
|
|
202
|
+
title = md.parent.name if md.stem == "index" else md.stem
|
|
203
|
+
title = title.replace("-", " ").replace("_", " ").title()
|
|
204
|
+
desc_part = f": {desc}" if desc else ""
|
|
205
|
+
lines.append(f"{prefix}[{title}]({url}){desc_part}")
|
|
206
|
+
elif isinstance(item, dict):
|
|
207
|
+
for k, v in item.items():
|
|
208
|
+
if isinstance(v, str):
|
|
209
|
+
md = docs_dir / v
|
|
210
|
+
if md.exists():
|
|
211
|
+
url = md_to_url(v)
|
|
212
|
+
if url in seen_urls:
|
|
213
|
+
continue
|
|
214
|
+
seen_urls.add(url)
|
|
215
|
+
desc = get_description(md)
|
|
216
|
+
desc_part = f": {desc}" if desc else ""
|
|
217
|
+
lines.append(f"{prefix}[{k}]({url}){desc_part}")
|
|
218
|
+
elif isinstance(v, list):
|
|
219
|
+
# Nested section - plain text header, then recurse
|
|
220
|
+
lines.append(f"{prefix}{k}")
|
|
221
|
+
process_items(v, indent + 1)
|
|
222
|
+
|
|
223
|
+
# Top-level nav items become ## sections
|
|
224
|
+
for item in nav:
|
|
225
|
+
if isinstance(item, str):
|
|
226
|
+
process_items([item], indent=0)
|
|
227
|
+
elif isinstance(item, dict):
|
|
228
|
+
for k, v in item.items():
|
|
229
|
+
if isinstance(v, list):
|
|
230
|
+
lines.extend(["", f"## {k}"])
|
|
231
|
+
process_items(v, indent=0)
|
|
232
|
+
else:
|
|
233
|
+
process_items([{k: v}], indent=0)
|
|
234
|
+
else:
|
|
235
|
+
for md in sorted(docs_dir.rglob("*.md")):
|
|
236
|
+
rel = md.relative_to(docs_dir).as_posix()
|
|
237
|
+
url = md_to_url(rel)
|
|
238
|
+
if url in seen_urls:
|
|
239
|
+
continue
|
|
240
|
+
seen_urls.add(url)
|
|
241
|
+
desc = get_description(md)
|
|
242
|
+
title = md.stem.replace("-", " ").replace("_", " ").title()
|
|
243
|
+
desc_part = f": {desc}" if desc else ""
|
|
244
|
+
lines.append(f"- [{title}]({url}){desc_part}")
|
|
245
|
+
|
|
246
|
+
(site_dir / "llms.txt").write_text("\n".join(lines))
|
|
247
|
+
print("Generated llms.txt")
|
|
248
|
+
|
|
249
|
+
|
|
137
250
|
def postprocess_site(
|
|
138
251
|
site_dir: str | Path = "site",
|
|
139
252
|
docs_dir: str | Path = "docs",
|
|
@@ -148,6 +261,7 @@ def postprocess_site(
|
|
|
148
261
|
add_json_ld: bool = False,
|
|
149
262
|
add_css: bool = True,
|
|
150
263
|
add_copy_llm: bool = True,
|
|
264
|
+
add_llms_txt: bool = True,
|
|
151
265
|
verbose: bool = True,
|
|
152
266
|
use_processes: bool = True,
|
|
153
267
|
workers: int | None = None,
|
|
@@ -250,6 +364,9 @@ def postprocess_site(
|
|
|
250
364
|
|
|
251
365
|
print(f"✅ Postprocessing complete: {processed}/{len(html_files)} files processed")
|
|
252
366
|
|
|
367
|
+
if add_llms_txt:
|
|
368
|
+
generate_llms_txt(site_dir, docs_dir, site_url)
|
|
369
|
+
|
|
253
370
|
|
|
254
371
|
if __name__ == "__main__":
|
|
255
372
|
postprocess_site()
|
|
@@ -136,7 +136,7 @@ def build_git_map(file_paths: list[str] | list[Path]) -> tuple[str | None, dict[
|
|
|
136
136
|
str(repo_root),
|
|
137
137
|
"log",
|
|
138
138
|
"--name-only",
|
|
139
|
-
"--pretty=format:%ad\t%ae",
|
|
139
|
+
"--pretty=format:%H\t%ad\t%ae",
|
|
140
140
|
"--date=format:%Y-%m-%d %H:%M:%S %z",
|
|
141
141
|
"--",
|
|
142
142
|
*[str(p) for p in rel_paths],
|
|
@@ -147,17 +147,18 @@ def build_git_map(file_paths: list[str] | list[Path]) -> tuple[str | None, dict[
|
|
|
147
147
|
except subprocess.CalledProcessError:
|
|
148
148
|
return repo_url, git_data
|
|
149
149
|
|
|
150
|
+
current_commit = None
|
|
150
151
|
current_date = None
|
|
151
152
|
current_email = None
|
|
152
153
|
for line in output:
|
|
153
154
|
if not line.strip():
|
|
154
155
|
continue
|
|
155
156
|
parts = line.split("\t")
|
|
156
|
-
if len(parts) ==
|
|
157
|
-
current_date, current_email = parts
|
|
157
|
+
if len(parts) == 3:
|
|
158
|
+
current_commit, current_date, current_email = parts
|
|
158
159
|
continue
|
|
159
160
|
|
|
160
|
-
if current_date and current_email:
|
|
161
|
+
if current_commit and current_date and current_email:
|
|
161
162
|
abs_path = (repo_root / line.strip()).resolve()
|
|
162
163
|
key = str(abs_path)
|
|
163
164
|
entry = git_data.setdefault(
|
|
@@ -166,11 +167,13 @@ def build_git_map(file_paths: list[str] | list[Path]) -> tuple[str | None, dict[
|
|
|
166
167
|
"creation_date": current_date,
|
|
167
168
|
"last_modified_date": current_date,
|
|
168
169
|
"emails": {},
|
|
170
|
+
"commits": {},
|
|
169
171
|
},
|
|
170
172
|
)
|
|
171
173
|
entry.setdefault("last_modified_date", current_date)
|
|
172
174
|
entry["creation_date"] = current_date
|
|
173
175
|
entry["emails"][current_email] = entry["emails"].get(current_email, 0) + 1
|
|
176
|
+
entry["commits"].setdefault(current_email, current_commit)
|
|
174
177
|
|
|
175
178
|
return repo_url, git_data
|
|
176
179
|
|
|
@@ -436,21 +439,22 @@ def process_html(
|
|
|
436
439
|
let rawUrl = editBtn.href.replace('github.com', 'raw.githubusercontent.com');
|
|
437
440
|
rawUrl = rawUrl.replace('/blob/', '/').replace('/tree/', '/');
|
|
438
441
|
|
|
439
|
-
|
|
442
|
+
async function getContent() {{
|
|
440
443
|
const response = await fetch(rawUrl);
|
|
441
444
|
let markdown = await response.text();
|
|
442
|
-
|
|
443
445
|
if (markdown.startsWith('---')) {{
|
|
444
446
|
const frontMatterEnd = markdown.indexOf('\\n---\\n', 3);
|
|
445
|
-
if (frontMatterEnd !== -1)
|
|
446
|
-
markdown = markdown.substring(frontMatterEnd + 5).trim();
|
|
447
|
-
}}
|
|
447
|
+
if (frontMatterEnd !== -1) markdown = markdown.substring(frontMatterEnd + 5).trim();
|
|
448
448
|
}}
|
|
449
|
-
|
|
450
449
|
const title = document.querySelector('h1')?.textContent || document.title;
|
|
451
|
-
|
|
450
|
+
return `# ${{title}}\\n\\nSource: ${{window.location.href}}\\n\\n---\\n\\n${{markdown}}`;
|
|
451
|
+
}}
|
|
452
452
|
|
|
453
|
-
|
|
453
|
+
try {{
|
|
454
|
+
const clipboardItem = new ClipboardItem({{
|
|
455
|
+
'text/plain': getContent().then(text => new Blob([text], {{ type: 'text/plain' }}))
|
|
456
|
+
}});
|
|
457
|
+
await navigator.clipboard.write([clipboardItem]);
|
|
454
458
|
button.innerHTML = checkIcon + ' Copied!';
|
|
455
459
|
setTimeout(() => {{ button.innerHTML = originalHTML; }}, 2000);
|
|
456
460
|
}} catch (err) {{
|
|
@@ -6,6 +6,7 @@ import re
|
|
|
6
6
|
from datetime import datetime
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any
|
|
9
|
+
from urllib.parse import urlparse
|
|
9
10
|
|
|
10
11
|
import requests
|
|
11
12
|
import yaml
|
|
@@ -97,14 +98,31 @@ def save_author_cache(cache: dict[str, dict[str, str | None]]) -> None:
|
|
|
97
98
|
print(f"{WARNING} Failed to save author cache: {e}")
|
|
98
99
|
|
|
99
100
|
|
|
101
|
+
def _github_repo_path(repo_url: str | None) -> str | None:
|
|
102
|
+
"""Return the owner/repo path for a GitHub repository URL."""
|
|
103
|
+
if not repo_url:
|
|
104
|
+
return None
|
|
105
|
+
parsed = urlparse(repo_url)
|
|
106
|
+
if parsed.hostname != "github.com":
|
|
107
|
+
return None
|
|
108
|
+
path = parsed.path.strip("/")
|
|
109
|
+
return path[:-4] if path.endswith(".git") else path or None
|
|
110
|
+
|
|
111
|
+
|
|
100
112
|
def resolve_github_user(
|
|
101
|
-
email: str,
|
|
113
|
+
email: str,
|
|
114
|
+
cache: dict[str, dict[str, str | None]],
|
|
115
|
+
repo_url: str | None = None,
|
|
116
|
+
commit_sha: str | None = None,
|
|
117
|
+
verbose: bool = True,
|
|
102
118
|
) -> dict[str, str | None]:
|
|
103
119
|
"""Resolve a single email to GitHub username and avatar, updating cache in-place.
|
|
104
120
|
|
|
105
121
|
Args:
|
|
106
122
|
email (str): The email address to resolve.
|
|
107
123
|
cache (dict): The author cache dict (modified in-place if new entry added).
|
|
124
|
+
repo_url (str, optional): GitHub repository URL used for commit API fallback.
|
|
125
|
+
commit_sha (str, optional): Commit SHA authored by the email.
|
|
108
126
|
verbose (bool): Whether to print API call info.
|
|
109
127
|
|
|
110
128
|
Returns:
|
|
@@ -113,8 +131,8 @@ def resolve_github_user(
|
|
|
113
131
|
if not email or not email.strip():
|
|
114
132
|
return {"username": None, "avatar": None}
|
|
115
133
|
|
|
116
|
-
# Return cached
|
|
117
|
-
if email in cache:
|
|
134
|
+
# Return complete cached results immediately. Incomplete cached entries may be refreshed from commit metadata.
|
|
135
|
+
if email in cache and cache[email].get("username") and cache[email].get("avatar"):
|
|
118
136
|
return cache[email]
|
|
119
137
|
|
|
120
138
|
# Parse username directly from GitHub noreply emails
|
|
@@ -127,6 +145,23 @@ def resolve_github_user(
|
|
|
127
145
|
cache[email] = {"username": username, "avatar": avatar}
|
|
128
146
|
return cache[email]
|
|
129
147
|
|
|
148
|
+
# Query the commit API when git history provides a commit for this email. This resolves authors whose commit email
|
|
149
|
+
# is linked to a GitHub account but hidden from user search.
|
|
150
|
+
if repo_path := _github_repo_path(repo_url):
|
|
151
|
+
if commit_sha:
|
|
152
|
+
try:
|
|
153
|
+
response = requests.get(
|
|
154
|
+
f"https://api.github.com/repos/{repo_path}/commits/{commit_sha}", timeout=TIMEOUT
|
|
155
|
+
)
|
|
156
|
+
if response.status_code == 200:
|
|
157
|
+
data = response.json()
|
|
158
|
+
author = data.get("author") or {}
|
|
159
|
+
if author.get("login") and author.get("avatar_url"):
|
|
160
|
+
cache[email] = {"username": author["login"], "avatar": author["avatar_url"]}
|
|
161
|
+
return cache[email]
|
|
162
|
+
except Exception:
|
|
163
|
+
pass
|
|
164
|
+
|
|
130
165
|
# Query GitHub REST API
|
|
131
166
|
if verbose:
|
|
132
167
|
print(f"Running GitHub REST API for author {email}")
|
|
@@ -173,10 +208,13 @@ def resolve_all_authors(
|
|
|
173
208
|
if not git_data:
|
|
174
209
|
return git_data
|
|
175
210
|
|
|
176
|
-
# Collect all unique emails across all files
|
|
211
|
+
# Collect all unique emails across all files, with one representative commit SHA per email.
|
|
177
212
|
all_emails: set[str] = set()
|
|
213
|
+
commits: dict[str, str] = {}
|
|
178
214
|
for entry in git_data.values():
|
|
179
215
|
all_emails.update(entry.get("emails", {}).keys())
|
|
216
|
+
for email, commit in entry.get("commits", {}).items():
|
|
217
|
+
commits.setdefault(email, commit)
|
|
180
218
|
if default_author:
|
|
181
219
|
all_emails.add(default_author)
|
|
182
220
|
all_emails.discard("")
|
|
@@ -189,8 +227,9 @@ def resolve_all_authors(
|
|
|
189
227
|
cache_modified = False
|
|
190
228
|
|
|
191
229
|
for email in sorted(all_emails):
|
|
192
|
-
|
|
193
|
-
|
|
230
|
+
cached = cache.get(email, {})
|
|
231
|
+
if email not in cache or (commits.get(email) and not (cached.get("username") and cached.get("avatar"))):
|
|
232
|
+
resolve_github_user(email, cache, repo_url=repo_url, commit_sha=commits.get(email), verbose=verbose)
|
|
194
233
|
cache_modified = True
|
|
195
234
|
|
|
196
235
|
if cache_modified:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|