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.
Files changed (16) hide show
  1. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/PKG-INFO +7 -5
  2. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/README.md +6 -4
  3. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/PKG-INFO +7 -5
  4. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/__init__.py +1 -1
  5. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/main.py +25 -1
  6. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/postprocess.py +117 -0
  7. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/processor.py +16 -12
  8. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/plugin/utils.py +45 -6
  9. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/LICENSE +0 -0
  10. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/SOURCES.txt +0 -0
  11. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/dependency_links.txt +0 -0
  12. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/entry_points.txt +0 -0
  13. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/requires.txt +0 -0
  14. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/mkdocs_ultralytics_plugin.egg-info/top_level.txt +0 -0
  15. {mkdocs_ultralytics_plugin-0.2.4 → mkdocs_ultralytics_plugin-0.2.5}/pyproject.toml +0 -0
  16. {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.4
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.com/), 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.
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://developer.x.com/en/docs/x-for-websites/cards/overview/summary-card-with-large-image) meta tags to ensure your content looks great when shared on social platforms.
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-v3) license promotes open collaboration and knowledge sharing. See the [LICENSE](https://github.com/ultralytics/mkdocs/blob/main/LICENSE) file for details.
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.com/), 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.
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://developer.x.com/en/docs/x-for-websites/cards/overview/summary-card-with-large-image) meta tags to ensure your content looks great when shared on social platforms.
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-v3) license promotes open collaboration and knowledge sharing. See the [LICENSE](https://github.com/ultralytics/mkdocs/blob/main/LICENSE) file for details.
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.4
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.com/), 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.
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://developer.x.com/en/docs/x-for-websites/cards/overview/summary-card-with-large-image) meta tags to ensure your content looks great when shared on social platforms.
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-v3) license promotes open collaboration and knowledge sharing. See the [LICENSE](https://github.com/ultralytics/mkdocs/blob/main/LICENSE) file for details.
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,6 +1,6 @@
1
1
  # Ultralytics 🚀 AGPL-3.0 License - https://ultralytics.com/license
2
2
 
3
- __version__ = "0.2.4"
3
+ __version__ = "0.2.5"
4
4
 
5
5
  from .main import MetaPlugin
6
6
  from .postprocess import postprocess_site
@@ -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) == 2:
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
- try {{
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
- const content = `# ${{title}}\\n\\nSource: ${{window.location.href}}\\n\\n---\\n\\n${{markdown}}`;
450
+ return `# ${{title}}\\n\\nSource: ${{window.location.href}}\\n\\n---\\n\\n${{markdown}}`;
451
+ }}
452
452
 
453
- await navigator.clipboard.writeText(content);
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, cache: dict[str, dict[str, str | None]], verbose: bool = True
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 result if available
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
- if email not in cache:
193
- resolve_github_user(email, cache, verbose=verbose)
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: