From e3ae0ff413b9c143a569b75b34310404fc36de27 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:48:10 -0400 Subject: [PATCH] feat(site): publish llms.txt and llms-full.txt for agents that browse 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 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 --- .gitignore | 2 ++ scripts/site/build_site.py | 67 +++++++++++++++++++++++++++++++++++ scripts/site/template.html.j2 | 1 + 3 files changed, 70 insertions(+) diff --git a/.gitignore b/.gitignore index 0a313e97..ddbcb92d 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,5 @@ build/ # Staged Pages tree (scripts/site/stage_public.py) _site/ docs/sitemap.xml +docs/llms.txt +docs/llms-full.txt diff --git a/scripts/site/build_site.py b/scripts/site/build_site.py index f70dbc9b..976f0dbb 100644 --- a/scripts/site/build_site.py +++ b/scripts/site/build_site.py @@ -502,6 +502,72 @@ def write_sitemap(repo_root: Path, out_dir: Path, canonical: str) -> None: print(f"Wrote {out_dir / 'sitemap.xml'} ({len(urls)} URLs)") + +RAW = "https://raw.githubusercontent.com/TMHSDigital/Blender-Developer-Tools/main/" +TREE = "https://github.com/TMHSDigital/Blender-Developer-Tools/tree/main/" + + +def _description(path: Path) -> str: + meta, _ = parse_frontmatter(path.read_text(encoding="utf-8", errors="replace")) + desc = meta.get("description", "").strip() + if len(desc) >= 2 and desc[0] == desc[-1] and desc[0] in "\"'": + desc = desc[1:-1].replace('\\"', '"') + return " ".join(desc.split()) + + +def write_llms_txt(repo_root: Path, out_dir: Path, site: dict) -> None: + """llms.txt (llmstxt.org index) and llms-full.txt (every skill plus the rules + summary) so an agent that browses the site can load the pack without + installing the plugin (#384). Links point at raw files on main.""" + canonical = (site.get("canonical") or "").rstrip("/") + "/" + lines = [ + "# Blender Developer Tools", + "", + f"> {site.get('description', '').strip()}", + "", + "Targets Blender 5.2 LTS with a 4.5 LTS fallback. Install as a Claude Code plugin with " + "`/plugin marketplace add TMHSDigital/Blender-Developer-Tools@plugin-dist`, or as a Cursor " + "local plugin from the `plugin-dist` branch. Every skill below is plain Markdown you can read " + f"directly. Full text of all skills and rules: {canonical}llms-full.txt", + "", + "## Skills", + "", + ] + skills = sorted((repo_root / "skills").glob("*/SKILL.md")) + for p in skills: + name = p.parent.name + lines.append(f"- [{name}]({RAW}skills/{name}/SKILL.md): {_description(p)}") + lines += ["", "## Rules", ""] + for p in sorted((repo_root / "rules").glob("*.mdc")): + lines.append(f"- [{p.stem}]({RAW}rules/{p.name}): {_description(p)}") + lines += [ + "", + "## Examples", + "", + f"- [Examples gallery]({canonical}gallery/): runnable headless scripts, each asserting one " + "API contract and exiting non-zero when it breaks, smoke-tested on 5.2 LTS and 4.5 LTS", + f"- [examples/]({TREE}examples): source of every example", + "", + "## Optional", + "", + f"- [snippets/]({TREE}snippets): small standalone patterns", + f"- [templates/]({TREE}templates): add-on, headless batch and asset-pipeline starters", + f"- [Rules summary for Claude Code]({RAW}claude/blender-rules.md)", + "", + ] + (out_dir / "llms.txt").write_text("\n".join(lines), encoding="utf-8", newline="\n") + + full = ["# Blender Developer Tools: all skills and rules", ""] + for p in skills: + _, body = parse_frontmatter(p.read_text(encoding="utf-8", errors="replace")) + full += [f"", body.strip(), ""] + rules_md = repo_root / "claude" / "blender-rules.md" + if rules_md.is_file(): + full += ["", rules_md.read_text(encoding="utf-8").strip(), ""] + (out_dir / "llms-full.txt").write_text("\n".join(full), encoding="utf-8", newline="\n") + print(f"Wrote {out_dir / 'llms.txt'} ({len(skills)} skills) and llms-full.txt") + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( @@ -621,6 +687,7 @@ def main(): print(f"Wrote {out_dir / '404.html'}") write_sitemap(repo_root, out_dir, site.get("canonical", "")) + write_llms_txt(repo_root, out_dir, site) fonts_src = template_dir / "fonts" fonts_dst = out_dir / "fonts" diff --git a/scripts/site/template.html.j2 b/scripts/site/template.html.j2 index 74d870fe..32670e73 100644 --- a/scripts/site/template.html.j2 +++ b/scripts/site/template.html.j2 @@ -7,6 +7,7 @@ {% if site.canonical %}{% endif %} {% if site.favicon %}{% endif %} +