Skip to content

Commit 7e4d109

Browse files
TMHSDigitalclaude
andcommitted
feat(catalog): no-JS rendering, generated category filters, real build date, a11y
Build date (#113): the footer no longer shows the visitor's current date. pages.yml stamps it at deploy time from the deployed commit's date. Discoverability (#114): sync_from_registry pre-renders every tool card and the hero stats into index.html, so crawlers, link unfurlers and no-JS visitors see the full catalog; the script rebuilds on top. og:image and twitter:image are absolute URLs, the favicon is a 32px PNG plus an apple-touch-icon, and the hero uses a 2 KB WebP instead of the 754 KB logo. robots.txt and sitemap.xml are generated. Filters (#115): each registry entry carries a category, validated against a fixed list. Filter chips are generated from the categories in use, so all 17 tools appear under a chip (8 matched none before). A status select was added, and search, category, and status now combine and persist in the URL (?q=&cat=&status=). Accessibility (#116): labeled search input, aria-pressed on chips, aria-expanded/aria-controls on the mobile nav toggle, a polite live region for result counts, guarded localStorage access, and the search icon now sits inside the input. Closes #113 Closes #114 Closes #115 Closes #116 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: TMHSDigital <154358121+TMHSDigital@users.noreply.github.com>
1 parent 5cb6287 commit 7e4d109

12 files changed

Lines changed: 494 additions & 77 deletions

‎.github/workflows/pages.yml‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,15 @@ jobs:
3434
mkdir -p docs/assets
3535
cp -r assets/* docs/assets/ 2>/dev/null || true
3636
37+
# The footer date is the date of the deployed commit, stamped at deploy
38+
# time. It is never computed in the browser, so it cannot claim the
39+
# catalog is fresher than it is.
40+
- name: Stamp footer with deployed commit date
41+
run: |
42+
updated="$(git log -1 --format=%cs)"
43+
sed -i "s|<span id=\"buildDate\"></span>|<span id=\"buildDate\">Updated ${updated}</span>|" docs/index.html
44+
grep -q "Updated ${updated}" docs/index.html
45+
3746
- uses: actions/configure-pages@v6
3847

3948
- uses: actions/upload-pages-artifact@v5

‎assets/apple-touch-icon.png‎

20.4 KB
Loading

‎assets/favicon-32.png‎

1.46 KB
Loading

‎assets/logo-hero.webp‎

2.08 KB
Loading

‎docs/index.html‎

Lines changed: 238 additions & 76 deletions
Large diffs are not rendered by default.

‎docs/robots.txt‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
User-agent: *
2+
Allow: /
3+
4+
Sitemap: https://tmhsdigital.github.io/Developer-Tools-Directory/sitemap.xml

‎docs/sitemap.xml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3+
<url><loc>https://tmhsdigital.github.io/Developer-Tools-Directory/</loc></url>
4+
</urlset>

‎registry.json‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"slug": "cfx-developer-tools",
66
"description": "AI-powered Cursor IDE plugin for FiveM and RedM resource development",
77
"type": "cursor-plugin",
8+
"category": "game-dev",
89
"homepage": "https://tmhsdigital.github.io/CFX-Developer-Tools/",
910
"skills": 9,
1011
"rules": 6,
@@ -34,6 +35,7 @@
3435
"slug": "unity-developer-tools",
3536
"description": "Cursor IDE plugin for Unity game development with URP, HDRP, and Built-in render pipeline support",
3637
"type": "cursor-plugin",
38+
"category": "game-dev",
3739
"homepage": "https://tmhsdigital.github.io/Unity-Developer-Tools/",
3840
"skills": 18,
3941
"rules": 8,
@@ -61,6 +63,7 @@
6163
"slug": "docker-developer-tools",
6264
"description": "Cursor IDE plugin for Docker workflows with container management, Compose, and Dockerfile support",
6365
"type": "cursor-plugin",
66+
"category": "infrastructure",
6467
"homepage": "https://tmhsdigital.github.io/Docker-Developer-Tools/",
6568
"skills": 17,
6669
"rules": 10,
@@ -86,6 +89,7 @@
8689
"slug": "home-lab-developer-tools",
8790
"description": "Home lab and Raspberry Pi workflows for Cursor with Docker Compose, monitoring, DNS, networking, and security",
8891
"type": "cursor-plugin",
92+
"category": "infrastructure",
8993
"homepage": "https://tmhsdigital.github.io/Home-Lab-Developer-Tools/",
9094
"skills": 22,
9195
"rules": 11,
@@ -111,6 +115,7 @@
111115
"slug": "mobile-app-developer-tools",
112116
"description": "Cursor plugin for mobile app development with React Native, Expo, and Flutter support",
113117
"type": "cursor-plugin",
118+
"category": "mobile",
114119
"homepage": "https://tmhsdigital.github.io/Mobile-App-Developer-Tools/",
115120
"skills": 43,
116121
"rules": 12,
@@ -136,6 +141,7 @@
136141
"slug": "plaid-developer-tools",
137142
"description": "Cursor plugin and MCP companion for building on the Plaid API",
138143
"type": "cursor-plugin",
144+
"category": "fintech",
139145
"homepage": "https://tmhsdigital.github.io/Plaid-Developer-Tools/",
140146
"skills": 17,
141147
"rules": 7,
@@ -160,6 +166,7 @@
160166
"slug": "monday-cursor-plugin",
161167
"description": "Cursor IDE plugin for Monday.com with boards, items, sprints, docs, dashboards, and GraphQL",
162168
"type": "cursor-plugin",
169+
"category": "productivity",
163170
"homepage": "https://tmhsdigital.github.io/Monday-Cursor-Plugin/",
164171
"skills": 21,
165172
"rules": 8,
@@ -184,6 +191,7 @@
184191
"slug": "steam-cursor-plugin",
185192
"description": "Steam and Steamworks integration for Cursor IDE for game developers, modders, and power users",
186193
"type": "cursor-plugin",
194+
"category": "game-dev",
187195
"homepage": "https://tmhsdigital.github.io/Steam-Cursor-Plugin/",
188196
"skills": 30,
189197
"rules": 9,
@@ -208,6 +216,7 @@
208216
"slug": "steam-mcp",
209217
"description": "MCP server for Steam and Steamworks APIs with store data, player stats, reviews, achievements, and workshop tools",
210218
"type": "mcp-server",
219+
"category": "game-dev",
211220
"homepage": "https://www.npmjs.com/package/@tmhs/steam-mcp",
212221
"skills": 0,
213222
"rules": 0,
@@ -233,6 +242,7 @@
233242
"slug": "developer-tools-mcp",
234243
"description": "MCP server providing agent-callable read and write access to the TMHSDigital developer-tools ecosystem: registry, fleet status, drift checking, and standards management",
235244
"type": "mcp-server",
245+
"category": "developer-tooling",
236246
"homepage": "https://tmhsdigital.github.io/Developer-Tools-MCP/",
237247
"skills": 0,
238248
"rules": 0,
@@ -258,6 +268,7 @@
258268
"slug": "cfx-mcp",
259269
"description": "MCP server providing agent-callable tools for CFX/FiveM resource development, server management, and community platform integration.",
260270
"type": "mcp-server",
271+
"category": "game-dev",
261272
"homepage": "https://tmhsdigital.github.io/cfx-mcp/",
262273
"skills": 0,
263274
"rules": 0,
@@ -276,6 +287,7 @@
276287
"slug": "blender-developer-tools",
277288
"description": "Cursor and Claude Code skills, rules, snippets, and templates for Blender Python add-on and scripting development",
278289
"type": "cursor-plugin",
290+
"category": "3d-media",
279291
"homepage": "https://tmhsdigital.github.io/Blender-Developer-Tools/",
280292
"skills": 18,
281293
"rules": 9,
@@ -303,6 +315,7 @@
303315
"slug": "local-ai-mcp",
304316
"description": "Unified MCP server for managing local model runtimes (Ollama, LM Studio, and more): provider-agnostic discovery, lifecycle, hardware-fit, and delegated inference.",
305317
"type": "mcp-server",
318+
"category": "ai",
306319
"homepage": "https://tmhsdigital.github.io/local-ai-mcp/",
307320
"skills": 0,
308321
"rules": 0,
@@ -328,6 +341,7 @@
328341
"slug": "screencast-mcp",
329342
"description": "MCP server for Windows screen recording, frame sampling, and minimal ffmpeg edits (capture, watch, trim/concat/convert).",
330343
"type": "mcp-server",
344+
"category": "3d-media",
331345
"homepage": "https://tmhsdigital.github.io/screencast-mcp/",
332346
"skills": 0,
333347
"rules": 0,
@@ -353,6 +367,7 @@
353367
"slug": "tailscale-mcp",
354368
"description": "MCP server for safe programmatic access to the local Tailscale daemon: tailnet discovery, SSH config generation, port sharing via Serve/Funnel, and latency matrices.",
355369
"type": "mcp-server",
370+
"category": "infrastructure",
356371
"homepage": "https://tmhsdigital.github.io/tailscale-mcp/",
357372
"skills": 0,
358373
"rules": 0,
@@ -378,6 +393,7 @@
378393
"slug": "godot-correctness-mcp",
379394
"description": "Offline static correctness for Godot 4.x GDScript projects: anti-pattern linting via tree-sitter, cross-version API lookup and diff (4.4/4.5), and scene/resource/registry validation. Headless, offline, CI-first.",
380395
"type": "mcp-server",
396+
"category": "game-dev",
381397
"homepage": "https://tmhsdigital.github.io/godot-correctness-mcp/",
382398
"skills": 0,
383399
"rules": 0,
@@ -403,6 +419,7 @@
403419
"slug": "bounty-radar-mcp",
404420
"description": "Paid MCP server exposing a GitHub bounty radar tool. One tool, github_bounty_radar, priced at $0.05 USDC on Base and settled over x402 via the CDP facilitator.",
405421
"type": "mcp-server",
422+
"category": "developer-tooling",
406423
"homepage": "https://tmhsdigital.github.io/bounty-radar-mcp/",
407424
"skills": 0,
408425
"rules": 0,

‎scripts/sync_from_registry.py‎

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,18 @@
2828
from __future__ import annotations
2929

3030
import argparse
31+
import html
3132
import json
3233
import re
3334
import sys
3435
from pathlib import Path
3536
from typing import Any
3637

38+
try: # imported as scripts.sync_from_registry (tests, scaffold)
39+
from scripts.validate_registry import CATEGORIES
40+
except ImportError: # run directly as scripts/sync_from_registry.py
41+
from validate_registry import CATEGORIES
42+
3743
REPO_ROOT = Path(__file__).resolve().parent.parent
3844
REGISTRY_PATH = REPO_ROOT / "registry.json"
3945
README_PATH = REPO_ROOT / "README.md"
@@ -225,6 +231,116 @@ def render_embedded_registry(entries: list[dict[str, Any]]) -> str:
225231
return script_safe_json(entries)
226232

227233

234+
STATUS_ORDER = ("active", "beta", "experimental", "maintenance", "deprecated", "archived")
235+
236+
237+
def render_filters(entries: list[dict[str, Any]]) -> str:
238+
"""Filter chips: type chips, then one chip per category in use (in
239+
CATEGORIES order), then a status select. Generated so a new category or
240+
tool can never be left without a filter (DTD#115)."""
241+
esc = html.escape
242+
used = {e.get("category") for e in entries}
243+
chips = [
244+
("all", "All"),
245+
("cursor-plugin", "Plugins"),
246+
("mcp-server", "MCP Servers"),
247+
] + [(slug, label) for slug, label in CATEGORIES.items() if slug in used]
248+
lines = []
249+
for value, label in chips:
250+
active = value == "all"
251+
lines.append(
252+
f' <button type="button" class="filter-btn{" active" if active else ""}" '
253+
f'data-filter="{esc(value)}" aria-pressed="{"true" if active else "false"}">'
254+
f"{esc(label)}</button>"
255+
)
256+
statuses = [s for s in STATUS_ORDER if any(e.get("status") == s for e in entries)]
257+
lines.append(' <label class="sr-only" for="statusFilter">Filter by status</label>')
258+
lines.append(' <select id="statusFilter" class="status-filter">')
259+
lines.append(' <option value="all">Any status</option>')
260+
for s in statuses:
261+
lines.append(f' <option value="{esc(s)}">{esc(s.capitalize())}</option>')
262+
lines.append(" </select>")
263+
return "\n".join(lines) + "\n"
264+
265+
266+
def render_static_cards(entries: list[dict[str, Any]]) -> str:
267+
"""Server-rendered tool cards so crawlers, link unfurlers, and visitors
268+
without JavaScript see the full catalog. The page script rebuilds the
269+
grid from the same registry data and layers filtering on top."""
270+
esc = html.escape
271+
out = []
272+
for e in entries:
273+
status = e.get("status", "active")
274+
is_mcp = e["type"] == "mcp-server"
275+
attrs = (
276+
f'data-type="{esc(e["type"])}" data-category="{esc(e.get("category", ""))}" '
277+
f'data-status="{esc(status)}" data-slug="{esc(e["slug"])}"'
278+
)
279+
parts = [f' <div class="tool-card" {attrs}>']
280+
badges = [
281+
f'<span class="card-badge {"badge-mcp" if is_mcp else "badge-plugin"}">'
282+
f'{"MCP Server" if is_mcp else "Plugin"}</span>'
283+
]
284+
if status != "active":
285+
badges.append(
286+
f'<span class="card-badge badge-status badge-status-{esc(status)}">{esc(status)}</span>'
287+
)
288+
parts.append(
289+
f' <div class="card-header"><span class="card-title">{esc(e["name"])}</span>'
290+
f'<div class="card-badges">{"".join(badges)}</div></div>'
291+
)
292+
parts.append(f' <p class="card-description">{esc(e.get("description", ""))}</p>')
293+
stats = []
294+
for field, label in (("skills", "skills"), ("rules", "rules"), ("mcpTools", "MCP tools")):
295+
if int(e.get(field) or 0) > 0:
296+
stats.append(
297+
f'<span class="card-stat"><strong>{int(e[field])}</strong> {label}</span>'
298+
)
299+
for key, value in (e.get("extras") or {}).items():
300+
if isinstance(value, int) and value > 0:
301+
stats.append(
302+
f'<span class="card-stat"><strong>{value:,}</strong> {esc(key)}</span>'
303+
)
304+
parts.append(f' <div class="card-stats">{"".join(stats)}</div>')
305+
topics = "".join(
306+
f'<span class="topic-tag">{esc(t)}</span>' for t in (e.get("topics") or [])[:5]
307+
)
308+
parts.append(f' <div class="card-topics">{topics}</div>')
309+
links = [
310+
f'<a class="card-link" href="https://github.com/{esc(e["repo"])}" '
311+
f'target="_blank" rel="noopener">Repository</a>'
312+
]
313+
if e.get("homepage"):
314+
links.append(
315+
f'<a class="card-link" href="{esc(e["homepage"])}" target="_blank" rel="noopener">Docs</a>'
316+
)
317+
if e.get("npm"):
318+
links.append(
319+
f'<a class="card-link" href="https://www.npmjs.com/package/{esc(e["npm"])}" '
320+
f'target="_blank" rel="noopener">npm</a>'
321+
)
322+
parts.append(f' <div class="card-links">{"".join(links)}</div>')
323+
parts.append(" </div>")
324+
out.append("\n".join(parts))
325+
return "\n".join(out) + "\n"
326+
327+
328+
SITEMAP_URL = "https://tmhsdigital.github.io/Developer-Tools-Directory/"
329+
330+
331+
def render_sitemap() -> str:
332+
return (
333+
'<?xml version="1.0" encoding="UTF-8"?>\n'
334+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n'
335+
f" <url><loc>{SITEMAP_URL}</loc></url>\n"
336+
"</urlset>\n"
337+
)
338+
339+
340+
def render_robots() -> str:
341+
return f"User-agent: *\nAllow: /\n\nSitemap: {SITEMAP_URL}sitemap.xml\n"
342+
343+
228344
def list_standards(standards_dir: Path = STANDARDS_DIR) -> list[str]:
229345
"""Return standard slugs present on disk (``*.md`` minus ``README``)."""
230346
return sorted(
@@ -459,8 +575,37 @@ def sync_index(entries: list[dict[str, Any]], check: bool, root: Path = REPO_ROO
459575
new, "standardsCount", str(len(present)), index_path
460576
)
461577

578+
# Server-rendered filters, cards, and hero stats (no-JS / crawler view).
579+
if "<!-- registry:filters:start -->" in new:
580+
new = replace_between(
581+
new, "<!-- registry:filters:start -->", "<!-- registry:filters:end -->",
582+
render_filters(entries), index_path,
583+
)
584+
if "<!-- registry:cards:start -->" in new:
585+
new = replace_between(
586+
new, "<!-- registry:cards:start -->", "<!-- registry:cards:end -->",
587+
render_static_cards(entries), index_path,
588+
)
589+
stats = aggregate_stats(entries)
590+
for elem_id, key in (
591+
("stat-repos", "repos"), ("stat-skills", "skills"),
592+
("stat-rules", "rules"), ("stat-mcp", "mcpTools"),
593+
):
594+
if f'id="{elem_id}"' in new:
595+
new = replace_element_text_by_id(new, elem_id, str(stats[key]), index_path)
596+
462597
html_drift = write_if_changed(index_path, current, new, check, root)
463-
return html_drift or search_drift
598+
599+
# Crawler files live next to the page; only managed when already present
600+
# so a minimal catalog is not forced to carry them.
601+
seo_drift = False
602+
for name, body in (("sitemap.xml", render_sitemap()), ("robots.txt", render_robots())):
603+
path = root / "docs" / name
604+
if path.is_file():
605+
seo_drift |= write_if_changed(
606+
path, path.read_text(encoding="utf-8"), body, check, root
607+
)
608+
return html_drift or search_drift or seo_drift
464609

465610

466611
def write_if_changed(

‎scripts/validate_registry.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,19 @@
4040
VALID_ARCHETYPES = {"deployed-service"}
4141
VALID_LICENSES = {"CC-BY-NC-ND-4.0", "CC-BY-NC-ND-4.0 AND MIT", "MIT", "Apache-2.0"}
4242
NPM_SCOPE = "@tmhs/"
43+
# Catalog filter categories, in display order. The catalog site renders one
44+
# filter chip per category in use (scripts/sync_from_registry.py).
45+
CATEGORIES: dict[str, str] = {
46+
"game-dev": "Game Dev",
47+
"3d-media": "3D & Media",
48+
"infrastructure": "Infrastructure",
49+
"ai": "Local AI",
50+
"mobile": "Mobile",
51+
"fintech": "Fintech",
52+
"productivity": "Productivity",
53+
"developer-tooling": "Developer Tooling",
54+
}
55+
DEFAULT_CATEGORY = "developer-tooling"
4356
COUNT_FIELDS = ("skills", "rules", "mcpTools")
4457

4558

@@ -86,6 +99,9 @@ def validate(data: Any) -> list[str]:
8699
if tool.get("license") not in VALID_LICENSES:
87100
errors.append("%s: license must be one of %s" % (label, sorted(VALID_LICENSES)))
88101

102+
if "category" in tool and tool["category"] not in CATEGORIES:
103+
errors.append("%s: category must be one of %s" % (label, list(CATEGORIES)))
104+
89105
topics = tool.get("topics", [])
90106
if isinstance(topics, list) and not all(isinstance(t, str) for t in topics):
91107
errors.append("%s: topics must be array of strings" % label)

0 commit comments

Comments
 (0)