From 607cf01b85437afdff26583ee04e5a8a8d93924b Mon Sep 17 00:00:00 2001 From: fOuttaMyPaint Date: Thu, 23 Jul 2026 21:52:36 -0400 Subject: [PATCH] feat: curate landing-page examples to a featured subset The landing page dumped all 40 example cards into its Render Result section, making it the longest scroll on the site (9948px at 1280px). A landing page should showcase, not enumerate - the full grid lives at /gallery/ with search, filters, and density controls since #113. Selection is data-driven: gallery.json entries may carry a featured_rank integer, build_site.py sorts by it into featured_examples, and the template renders that subset (falling back to the full list when nothing is curated). The panel counter is now honest about shown-vs-total ('8 of 40 frames'), and the gallery link moves from a below-the-fold footnote to a primary button above the grid. No controls on the landing page - that is what /gallery/ is for. Featured set (rank order): car-mirror-symmetry, soccer-ball-goldberg, damped-track-aim, light-link-studio, collision-hull-proxy, armature-bend, bmesh-gear, color-attribute-wheel - the pinned calibration members plus recent striking work, chosen for subject and palette range. Landing page height: 9948px -> 4847px at 1280px (-51%), 24788px -> 10203px at 390px (-59%). Signed-off-by: fOuttaMyPaint --- examples/gallery.json | 24 ++++++++++++++++-------- scripts/site/build_site.py | 13 +++++++++++++ scripts/site/template.html.j2 | 11 +++++++++-- 3 files changed, 38 insertions(+), 10 deletions(-) diff --git a/examples/gallery.json b/examples/gallery.json index 976ca98..299ccc3 100644 --- a/examples/gallery.json +++ b/examples/gallery.json @@ -86,7 +86,8 @@ "tags": [ "mesh", "bmesh" - ] + ], + "featured_rank": 7 }, { "name": "shader-node-group", @@ -170,7 +171,8 @@ "tags": [ "constraints", "animation" - ] + ], + "featured_rank": 3 }, { "name": "color-attribute-wheel", @@ -183,7 +185,8 @@ "mesh", "materials", "attributes" - ] + ], + "featured_rank": 8 }, { "name": "parent-inverse-orrery", @@ -219,7 +222,8 @@ "tags": [ "armature", "depsgraph" - ] + ], + "featured_rank": 6 }, { "name": "text-version-stamp", @@ -368,7 +372,8 @@ "tags": [ "rendering", "objects" - ] + ], + "featured_rank": 4 }, { "name": "collision-hull-proxy", @@ -380,7 +385,8 @@ "tags": [ "bmesh", "mesh" - ] + ], + "featured_rank": 5 }, { "name": "custom-normals-shade", @@ -456,7 +462,8 @@ "tags": [ "bmesh", "mesh" - ] + ], + "featured_rank": 2 }, { "name": "car-mirror-symmetry", @@ -468,7 +475,8 @@ "tags": [ "modifiers", "depsgraph" - ] + ], + "featured_rank": 1 }, { "name": "attribute-domain-shear", diff --git a/scripts/site/build_site.py b/scripts/site/build_site.py index 13ae6ed..c56e4c6 100644 --- a/scripts/site/build_site.py +++ b/scripts/site/build_site.py @@ -250,6 +250,16 @@ def load_examples(repo_root: Path) -> list[dict]: return examples +def pick_featured(examples: list[dict]) -> list[dict]: + """Landing-page showcase subset: entries carrying a ``featured_rank`` + integer in gallery.json, sorted by rank. Returns [] when nothing is + curated; the template then falls back to the full list.""" + return sorted( + (ex for ex in examples if isinstance(ex.get("featured_rank"), int)), + key=lambda ex: ex["featured_rank"], + ) + + def load_mcp_tools(repo_root: Path) -> list[dict]: mcp_file = repo_root / "mcp-tools.json" if not mcp_file.is_file(): @@ -365,6 +375,7 @@ def main(): skills = parse_skills(repo_root) rules = parse_rules(repo_root) examples = load_examples(repo_root) + featured = pick_featured(examples) mcp_tools = load_mcp_tools(repo_root) mcp_grouped = group_by_category(mcp_tools) changelog = parse_changelog(repo_root) @@ -378,6 +389,8 @@ def main(): "rule_count": len(rules), "examples": examples, "example_count": len(examples), + "featured_examples": featured, + "featured_count": len(featured), "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 5e14880..eb3c4c7 100644 --- a/scripts/site/template.html.j2 +++ b/scripts/site/template.html.j2 @@ -153,6 +153,11 @@ text-overflow: ellipsis; } .panel-cta { display: inline-block; margin-top: 1.25rem; font-weight: 500; font-size: 0.9rem; color: var(--select); } .panel-cta:hover { text-decoration: underline; } + /* featured subset: gallery link is the section's primary call to action */ + .cta-row { margin: -0.25rem 0 1.5rem; } + .btn-cta { display: inline-block; background: var(--select); color: #1a1b1e; font-weight: 500; + font-size: 0.92rem; border-radius: 4px; padding: 0.55rem 1.15rem; transition: filter 0.15s; } + .btn-cta:hover { filter: brightness(1.12); } /* skills */ .skill-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.4rem 2.25rem; } @@ -284,19 +289,21 @@
{% if examples %} + {% set shown = featured_examples if featured_examples else examples %}
Render Result - {{ example_count }} frames + {% if featured_examples %}{{ featured_count }} of {% endif %}{{ example_count }} frames

Every render here is a CI artifact.

These aren't mockups. Each example runs headless on Blender 4.5 LTS and 5.1 in the smoke workflow, asserts its own correctness, and exits non-zero if the API drifted. The render is what the code produced.

+

Browse all {{ example_count }} examples in the gallery →

- {% for ex in examples %} + {% for ex in shown %} {{ ex.name }} render