From f828299fef87904c7f30a1c3aeb0097ad98c7d55 Mon Sep 17 00:00:00 2001 From: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Date: Sun, 13 Sep 2026 13:05:05 -0400 Subject: [PATCH 1/3] feat: add sibling showcase/ category to the content schema Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com> Co-authored-by: Cursor --- .cursor-plugin/plugin.json | 3 ++- .github/workflows/pages.yml | 1 + .github/workflows/validate.yml | 20 +++++++++++++++++-- README.md | 4 ++-- scripts/build_gallery.py | 27 +++++++++++++++++++++++--- scripts/site/build_site.py | 35 +++++++++++++++++++++++----------- scripts/site/template.html.j2 | 30 +++++++++++++++++++++++++++++ showcase/gallery.json | 8 ++++++++ 8 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 showcase/gallery.json diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 34eb6dd..d562c10 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -136,5 +136,6 @@ "examples/vse-gamma-cross", "examples/vse-linear-modifiers", "examples/wave-displace" - ] + ], + "showcase": [] } diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 00597bb..64e0b0e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -18,6 +18,7 @@ on: - ".cursor-plugin/plugin.json" - "assets/**" - "examples/**" + - "showcase/**" - "docs/gallery/**" - "scripts/build_gallery.py" - "scripts/site/**" diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 165d376..825bd26 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -184,7 +184,7 @@ jobs: ) # Every manifest path must exist on disk. - for key in ('skills', 'rules', 'snippets', 'templates', 'examples'): + for key in ('skills', 'rules', 'snippets', 'templates', 'examples', 'showcase'): for path in manifest.get(key, []): if not os.path.exists(path): errors.append(f'{key}: manifest lists missing path {path}') @@ -200,6 +200,9 @@ jobs: 'examples': sorted( d for d in glob.glob('examples/*') if os.path.isdir(d) ), + 'showcase': sorted( + d for d in glob.glob('showcase/*') if os.path.isdir(d) + ), } for key, paths in expected.items(): listed = {p.replace('\\', '/') for p in manifest.get(key, [])} @@ -252,6 +255,13 @@ jobs: if os.path.isdir(os.path.join('examples', d)) and os.path.exists(os.path.join('examples', d, 'README.md')) ]) + showcase_count = 0 + if os.path.isdir('showcase'): + showcase_count = len([ + d for d in os.listdir('showcase') + if os.path.isdir(os.path.join('showcase', d)) + and os.path.exists(os.path.join('showcase', d, 'README.md')) + ]) readme = open('README.md').read() if f'{skill_count} skills' not in readme: @@ -265,13 +275,19 @@ jobs: errors.append(f'README snippet count mismatch (expected "{snippet_count} snippets" substring)') if f'{example_count} examples' not in readme: errors.append(f'README example count mismatch (expected "{example_count} examples" substring)') + showcase_word = 'piece' if showcase_count == 1 else 'pieces' + showcase_needle = f'{showcase_count} showcase {showcase_word}' + if showcase_needle not in readme: + errors.append( + f'README showcase count mismatch (expected "{showcase_needle}" substring)' + ) if errors: for e in errors: print(f'::error::{e}', file=sys.stderr) sys.exit(1) - print(f'Counts verified: {skill_count} skills, {rule_count} rules, {template_count} {template_word}, {snippet_count} snippets, {example_count} examples') + print(f'Counts verified: {skill_count} skills, {rule_count} rules, {template_count} {template_word}, {snippet_count} snippets, {example_count} examples, {showcase_needle}') PYEOF validate-harness: diff --git a/README.md b/README.md index d0b5246..25a849a 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@

- 16 skills  •  9 rules  •  3 templates  •  27 snippets  •  59 examples + 16 skills  •  9 rules  •  3 templates  •  27 snippets  •  59 examples  •  0 showcase pieces

@@ -36,7 +36,7 @@ ## Overview -This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, and 59 examples** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable. +This repository ships **16 skills, 9 rules, 3 templates, 27 snippets, 59 examples, and 0 showcase pieces** for Blender Python development targeting Blender 5.2 LTS (current stable) with Blender 4.5 LTS fallback support. Blender 5.1 is prior stable. The content is consumed by AI coding agents (Cursor, Claude Code, any MCP-capable client) when working on Blender add-ons, geometry nodes scripts, batch pipelines, or animation tooling. There is no build step. Edit the markdown and Python files directly. diff --git a/scripts/build_gallery.py b/scripts/build_gallery.py index 4d413a3..1f14c04 100644 --- a/scripts/build_gallery.py +++ b/scripts/build_gallery.py @@ -17,8 +17,10 @@ writes ONLY under docs/gallery/ so it never collides with the landing build's docs/index.html, docs/fonts/, or docs/assets/. -examples/gallery.json is the source of truth. Run after editing gallery.json, -an example script, or an example README: +examples/gallery.json is the source of truth for examples. showcase/gallery.json +is the source of truth for showcase pieces (``pieces`` key). This script merges +both into one docs/gallery/ index so each tree owns its JSON. Run after editing +either file, an example or showcase script, or a README: python scripts/build_gallery.py @@ -38,12 +40,29 @@ REPO = Path(__file__).resolve().parent.parent DATA = REPO / "examples" / "gallery.json" +SHOWCASE_DATA = REPO / "showcase" / "gallery.json" OUT_DIR = REPO / "docs" / "gallery" # Soft cap for gallery index card alt text (accessibility + layout). _ALT_CAP = 160 +def load_gallery_entries() -> tuple[dict, list]: + """Examples gallery metadata plus concatenated example + showcase cards.""" + data = json.loads(DATA.read_text(encoding="utf-8")) + entries = list(data["examples"]) + if SHOWCASE_DATA.is_file(): + show = json.loads(SHOWCASE_DATA.read_text(encoding="utf-8")) + for piece in show.get("pieces", []): + item = dict(piece) + tags = list(item.get("tags") or []) + if "showcase" not in tags: + tags.append("showcase") + item["tags"] = tags + entries.append(item) + return data, entries + + def first_sentence(text: str) -> str: """First sentence of *text*, splitting on period-followed-by-whitespace. @@ -892,7 +911,9 @@ def build_index(data: dict, *, base: str, repo_root_url: str, site: str) -> str: def main() -> int: - data = json.loads(DATA.read_text(encoding="utf-8")) + data, examples = load_gallery_entries() + data = dict(data) + data["examples"] = examples base = data["repoBaseUrl"].rstrip("/") repo_root_url = base.split("/tree/")[0] # strip /tree/ -> repo home site = data.get("siteBaseUrl", "").rstrip("/") diff --git a/scripts/site/build_site.py b/scripts/site/build_site.py index c56e4c6..bf5a771 100644 --- a/scripts/site/build_site.py +++ b/scripts/site/build_site.py @@ -233,21 +233,31 @@ def parse_changelog(repo_root: Path, max_entries: int = 2) -> list[dict]: return entries -def load_examples(repo_root: Path) -> list[dict]: - """Read examples/gallery.json (the gallery source of truth) when present. +def _hero_site(hero: str) -> str: + """Repo-root ``docs/...`` hero path → site-relative path under Pages.""" + return hero[len("docs/"):] if hero.startswith("docs/") else hero + - Hero paths in gallery.json are repo-root-relative (``docs/gallery/...``); - the deployed site serves ``docs/`` as its root, so expose a site-relative - ``heroSite`` alongside each entry.""" - gallery_path = repo_root / "examples" / "gallery.json" +def load_gallery_items(repo_root: Path, relpath: str, key: str) -> list[dict]: + """Read a gallery JSON file and attach ``heroSite`` on each entry.""" + gallery_path = repo_root / relpath if not gallery_path.is_file(): return [] data = load_json(gallery_path) - examples = data.get("examples", []) if isinstance(data, dict) else [] - for ex in examples: - hero = ex.get("hero", "") - ex["heroSite"] = hero[len("docs/"):] if hero.startswith("docs/") else hero - return examples + items = data.get(key, []) if isinstance(data, dict) else [] + for item in items: + item["heroSite"] = _hero_site(item.get("hero", "")) + return items + + +def load_examples(repo_root: Path) -> list[dict]: + """Read examples/gallery.json (the examples gallery source of truth).""" + return load_gallery_items(repo_root, "examples/gallery.json", "examples") + + +def load_showcase(repo_root: Path) -> list[dict]: + """Read showcase/gallery.json. Empty or absent is fine — not an error.""" + return load_gallery_items(repo_root, "showcase/gallery.json", "pieces") def pick_featured(examples: list[dict]) -> list[dict]: @@ -376,6 +386,7 @@ def main(): rules = parse_rules(repo_root) examples = load_examples(repo_root) featured = pick_featured(examples) + showcase = load_showcase(repo_root) mcp_tools = load_mcp_tools(repo_root) mcp_grouped = group_by_category(mcp_tools) changelog = parse_changelog(repo_root) @@ -391,6 +402,8 @@ def main(): "example_count": len(examples), "featured_examples": featured, "featured_count": len(featured), + "showcase": showcase, + "showcase_count": len(showcase), "snippet_count": len(plugin.get("snippets", [])), "template_count": len(plugin.get("templates", [])), # basenames for display: snippets/foo-bar.py -> foo-bar diff --git a/scripts/site/template.html.j2 b/scripts/site/template.html.j2 index eb3c4c7..7656eda 100644 --- a/scripts/site/template.html.j2 +++ b/scripts/site/template.html.j2 @@ -239,6 +239,7 @@