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 %} +