Skip to content

Commit 93e9e8f

Browse files
TMHSDigitalclaude
andauthored
feat(site): publish llms.txt and llms-full.txt for agents that browse (#433)
https://tmhsdigital.github.io/Blender-Developer-Tools/llms.txt returned 404. Agents that read docs from the web (Cursor @docs, web fetch) look for it, and it lets an agent use the pack without installing a plugin. The landing build now writes, next to sitemap.xml: - llms.txt (llmstxt.org format): the site description, the install lines, and every skill and rule with its full description, each linked to the raw file on main; plus examples, snippets and templates; - llms-full.txt: every SKILL.md body plus the generated rules summary. The landing <head> links llms.txt, so the site link gate fails if the build ever stops writing it. Both files are build outputs (gitignored), like index.html and sitemap.xml. Closes #384 Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 92d033b commit 93e9e8f

3 files changed

Lines changed: 70 additions & 0 deletions

File tree

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,5 @@ build/
5252
# Staged Pages tree (scripts/site/stage_public.py)
5353
_site/
5454
docs/sitemap.xml
55+
docs/llms.txt
56+
docs/llms-full.txt

‎scripts/site/build_site.py‎

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -502,6 +502,72 @@ def write_sitemap(repo_root: Path, out_dir: Path, canonical: str) -> None:
502502
print(f"Wrote {out_dir / 'sitemap.xml'} ({len(urls)} URLs)")
503503

504504

505+
506+
RAW = "https://raw.githubusercontent.com/TMHSDigital/Blender-Developer-Tools/main/"
507+
TREE = "https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/"
508+
509+
510+
def _description(path: Path) -> str:
511+
meta, _ = parse_frontmatter(path.read_text(encoding="utf-8", errors="replace"))
512+
desc = meta.get("description", "").strip()
513+
if len(desc) >= 2 and desc[0] == desc[-1] and desc[0] in "\"'":
514+
desc = desc[1:-1].replace('\\"', '"')
515+
return " ".join(desc.split())
516+
517+
518+
def write_llms_txt(repo_root: Path, out_dir: Path, site: dict) -> None:
519+
"""llms.txt (llmstxt.org index) and llms-full.txt (every skill plus the rules
520+
summary) so an agent that browses the site can load the pack without
521+
installing the plugin (#384). Links point at raw files on main."""
522+
canonical = (site.get("canonical") or "").rstrip("/") + "/"
523+
lines = [
524+
"# Blender Developer Tools",
525+
"",
526+
f"> {site.get('description', '').strip()}",
527+
"",
528+
"Targets Blender 5.2 LTS with a 4.5 LTS fallback. Install as a Claude Code plugin with "
529+
"`/plugin marketplace add TMHSDigital/Blender-Developer-Tools@plugin-dist`, or as a Cursor "
530+
"local plugin from the `plugin-dist` branch. Every skill below is plain Markdown you can read "
531+
f"directly. Full text of all skills and rules: {canonical}llms-full.txt",
532+
"",
533+
"## Skills",
534+
"",
535+
]
536+
skills = sorted((repo_root / "skills").glob("*/SKILL.md"))
537+
for p in skills:
538+
name = p.parent.name
539+
lines.append(f"- [{name}]({RAW}skills/{name}/SKILL.md): {_description(p)}")
540+
lines += ["", "## Rules", ""]
541+
for p in sorted((repo_root / "rules").glob("*.mdc")):
542+
lines.append(f"- [{p.stem}]({RAW}rules/{p.name}): {_description(p)}")
543+
lines += [
544+
"",
545+
"## Examples",
546+
"",
547+
f"- [Examples gallery]({canonical}gallery/): runnable headless scripts, each asserting one "
548+
"API contract and exiting non-zero when it breaks, smoke-tested on 5.2 LTS and 4.5 LTS",
549+
f"- [examples/]({TREE}examples): source of every example",
550+
"",
551+
"## Optional",
552+
"",
553+
f"- [snippets/]({TREE}snippets): small standalone patterns",
554+
f"- [templates/]({TREE}templates): add-on, headless batch and asset-pipeline starters",
555+
f"- [Rules summary for Claude Code]({RAW}claude/blender-rules.md)",
556+
"",
557+
]
558+
(out_dir / "llms.txt").write_text("\n".join(lines), encoding="utf-8", newline="\n")
559+
560+
full = ["# Blender Developer Tools: all skills and rules", ""]
561+
for p in skills:
562+
_, body = parse_frontmatter(p.read_text(encoding="utf-8", errors="replace"))
563+
full += [f"<!-- skills/{p.parent.name}/SKILL.md -->", body.strip(), ""]
564+
rules_md = repo_root / "claude" / "blender-rules.md"
565+
if rules_md.is_file():
566+
full += ["<!-- claude/blender-rules.md -->", rules_md.read_text(encoding="utf-8").strip(), ""]
567+
(out_dir / "llms-full.txt").write_text("\n".join(full), encoding="utf-8", newline="\n")
568+
print(f"Wrote {out_dir / 'llms.txt'} ({len(skills)} skills) and llms-full.txt")
569+
570+
505571
def main():
506572
parser = argparse.ArgumentParser(description=__doc__)
507573
parser.add_argument(
@@ -621,6 +687,7 @@ def main():
621687
print(f"Wrote {out_dir / '404.html'}")
622688

623689
write_sitemap(repo_root, out_dir, site.get("canonical", ""))
690+
write_llms_txt(repo_root, out_dir, site)
624691

625692
fonts_src = template_dir / "fonts"
626693
fonts_dst = out_dir / "fonts"

‎scripts/site/template.html.j2‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<meta name="description" content="{{ plugin.description }}" />
88
{% if site.canonical %}<link rel="canonical" href="{{ site.canonical }}" />{% endif %}
99
{% if site.favicon %}<link rel="icon" href="{{ site.favicon }}" />{% endif %}
10+
<link rel="alternate" type="text/plain" title="llms.txt: the pack for AI agents" href="llms.txt" />
1011
<meta name="theme-color" content="#1a1b1e" />
1112
<meta name="color-scheme" content="dark" />
1213
<meta property="og:type" content="website" />

0 commit comments

Comments
 (0)