Skip to content

update to support descriptions with file links #8

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,8 @@ plugins:
markdown_description: Long description of my project.
sections:
Usage documentation:
- file1.md
- file2.md
file1.md: Description of file1
file2.md: Description of file2
```

The resulting `/llms.txt` file will be available at the root of your documentation. With the previous example, it will be accessible at https://myproject.com/llms.txt and will contain the following:
Expand All @@ -46,21 +46,22 @@ Long description of my project.

## Usage documentation

- [File1 title](https://myproject.com/file1.md)
- [File2 title](https://myproject.com/file2.md)
- [File1 title](https://myproject.com/file1.md): Description of file1
- [File2 title](https://myproject.com/file2.md): Description of file2
```

Each source file included in `sections` will have its own Markdown file available at the specified URL in the `/llms.txt`. See [Markdown generation](#markdown-generation) for more details.

File globbing is supported:
File globbing is supported, and you can mix files with and without descriptions:

```yaml title="mkdocs.yml"
plugins:
- llmstxt:
sections:
Usage documentation:
- index.md
- usage/*.md
index.md: Main documentation page

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you'd need the dashes here still, no? this doesn't look legal on the next line

Copy link
Author

@logan-markewich logan-markewich May 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So from what I understand:

  • if you are using descriptions, this format is correct
  • the old format (just a list using dashes) also still works

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But here you're using both formats at once, which is invalid: you'd at least need a trailing colon on the next line.

usage/*.md # Files without descriptions
api.md: API reference documentation
```

## Full output
Expand Down
18 changes: 14 additions & 4 deletions src/mkdocs_llmstxt/_internal/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ class _MDPageInfo(NamedTuple):
path_md: Path
md_url: str
content: str
description: str | None = None


class MkdocsLLMsTxtPlugin(BasePlugin[_PluginConfig]):
Expand Down Expand Up @@ -136,12 +137,18 @@ def on_page_content(self, html: str, *, page: Page, **kwargs: Any) -> str | None
base += "/"
md_url = urljoin(base, md_url)

# Get description if available in the config
description = None
if isinstance(file_list, dict) and page.file.src_uri in file_list:
description = file_list[page.file.src_uri]

self.md_pages[section_name].append(
_MDPageInfo(
title=page.title if page.title is not None else page.file.src_uri,
path_md=path_md,
md_url=md_url,
content=page_md,
description=description,
),
)

Expand Down Expand Up @@ -169,10 +176,13 @@ def on_post_build(self, *, config: MkDocsConfig, **kwargs: Any) -> None: # noqa

for section_name, file_list in self.md_pages.items():
markdown += f"## {section_name}\n\n"
for page_title, path_md, md_url, content in file_list:
path_md.write_text(content, encoding="utf8")
_logger.debug(f"Generated MD file to {path_md}")
markdown += f"- [{page_title}]({md_url})\n"
for page_info in file_list:
page_info.path_md.write_text(page_info.content, encoding="utf8")
_logger.debug(f"Generated MD file to {page_info.path_md}")
if page_info.description:
markdown += f"- [{page_info.title}]({page_info.md_url}): {page_info.description}\n"
else:
markdown += f"- [{page_info.title}]({page_info.md_url})\n"
markdown += "\n"

output_file.write_text(markdown, encoding="utf8")
Expand Down
138 changes: 138 additions & 0 deletions tests/test_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,148 @@

import pytest
from duty.tools import mkdocs
from mkdocs.config.defaults import MkDocsConfig
from mkdocs.structure.files import Files
from mkdocs.structure.pages import Page
from types import SimpleNamespace

from mkdocs_llmstxt._internal.plugin import MkdocsLLMsTxtPlugin


def test_plugin() -> None:
"""Run the plugin."""
with pytest.raises(expected_exception=SystemExit) as exc:
mkdocs.build()()
assert exc.value.code == 0


def test_page_descriptions() -> None:
"""Test that page descriptions are correctly handled and included in output."""
# Create a mock config
config = MkDocsConfig()
config.site_name = "Test Project"
config.site_description = "Test Description"
config.site_url = "https://test.com/"

# Create plugin instance with test configuration
plugin = MkdocsLLMsTxtPlugin()
plugin.load_config({
"sections": {
"Test Section": {
"page1.md": "Description of page 1",
"page2.md": "Description of page 2"
}
}
})

# Initialize plugin
plugin.on_config(config)

# Create mock file and page
file = SimpleNamespace(
src_uri="page1.md",
dest_uri="page1.html",
abs_dest_path="/tmp/page1.html",
url="page1.html"
)
page = Page(
title="Test Page",
file=file,
config=config
)

# Process page content
plugin.on_page_content("<html><body>Test content</body></html>", page=page)

# Check that the page info was stored correctly
assert len(plugin.md_pages["Test Section"]) == 1
page_info = plugin.md_pages["Test Section"][0]
assert page_info.title == "Test Page"
assert page_info.description == "Description of page 1"


def test_mixed_descriptions() -> None:
"""Test that mixing files with and without descriptions works correctly."""
# Create a mock config
config = MkDocsConfig()
config.site_name = "Test Project"
config.site_url = "https://test.com/"

# Create plugin instance with test configuration
plugin = MkdocsLLMsTxtPlugin()
plugin.load_config({
"sections": {
"Test Section": {
"page1.md": "Description of page 1",
"page2.md": None,
"page3.md": "Description of page 3"
}
}
})

# Initialize plugin
plugin.on_config(config)

# Create and process mock pages
for page_num in range(1, 4):
file = SimpleNamespace(
src_uri=f"page{page_num}.md",
dest_uri=f"page{page_num}.html",
abs_dest_path=f"/tmp/page{page_num}.html",
url=f"page{page_num}.html"
)
page = Page(
title=f"Test Page {page_num}",
file=file,
config=config
)
plugin.on_page_content("<html><body>Test content</body></html>", page=page)

# Check that all pages were processed
assert len(plugin.md_pages["Test Section"]) == 3

# Verify descriptions
descriptions = [info.description for info in plugin.md_pages["Test Section"]]
assert descriptions == ["Description of page 1", None, "Description of page 3"]

def test_no_descriptions() -> None:
"""Test that no descriptions are included when no descriptions are provided."""
# Create a mock config
config = MkDocsConfig()
config.site_name = "Test Project"
config.site_url = "https://test.com/"

# Create plugin instance with test configuration
plugin = MkdocsLLMsTxtPlugin()
plugin.load_config({
"sections": {
"Test Section": [
"page1.md",
"page2.md"
]
}
})

# Initialize plugin
plugin.on_config(config)

# Create and process mock pages
for page_num in range(1, 3):
file = SimpleNamespace(
src_uri=f"page{page_num}.md",
dest_uri=f"page{page_num}.html",
abs_dest_path=f"/tmp/page{page_num}.html",
url=f"page{page_num}.html"
)
page = Page(
title=f"Test Page {page_num}",
file=file,
config=config
)
plugin.on_page_content("<html><body>Test content</body></html>", page=page)

# Check that all pages were processed
assert len(plugin.md_pages["Test Section"]) == 2

descriptions = [info.description for info in plugin.md_pages["Test Section"]]
assert descriptions == [None, None]