Skip to content

feat(showcase): build the static gallery generator (#300) - #308

Merged
salazarsebas merged 3 commits into
salazarsebas:mainfrom
sanusishafii989:feat/showcase-static-gallery-generator
Aug 31, 2026
Merged

feat(showcase): build the static gallery generator (#300)#308
salazarsebas merged 3 commits into
salazarsebas:mainfrom
sanusishafii989:feat/showcase-static-gallery-generator

Conversation

@sanusishafii989

Copy link
Copy Markdown

Architectural decision: integrated static generator (Python, matching the existing sync.py pattern) that compiles catalog TOML metadata and README files into static mdBook pages alongside the docs build. Zero backend, zero database, 100% static output in a single build step.

Generator (cougr-site/generate-showcase.py):

  • Reads examples/catalog.toml for structured metadata (category, maturity, cougr_features, optional screenshot/testnet_contract)
  • Resolves and parses each example's README.md for descriptions and full content without manual duplication
  • Consumes design tokens directly from packages/tokens/tokens.json (single source of truth per docs/BRAND.md) as CSS custom properties

Output:

  • Gallery index page with client-side filtering by category and maturity (vanilla JS, no dependencies)
  • One detail page per example with badges, feature tags, README content (links rewritten to GitHub URLs), and preview image
  • Preview images copied when present; gracefully omitted when absent
  • Cougr Verified badge rendered when catalog provides verified field

CI integration:

  • Generator runs after sync.py verification, before mdbook build
  • Workflow triggers on catalog.toml, example READMEs, preview SVGs, and tokens.json changes in addition to existing paths

Closes #300

image

Architectural decision: integrated static generator (Python, matching
the existing sync.py pattern) that compiles catalog TOML metadata and
README files into static mdBook pages alongside the docs build. Zero
backend, zero database, 100% static output in a single build step.

Generator (cougr-site/generate-showcase.py):
- Reads examples/catalog.toml for structured metadata (category,
  maturity, cougr_features, optional screenshot/testnet_contract)
- Resolves and parses each example's README.md for descriptions and
  full content without manual duplication
- Consumes design tokens directly from packages/tokens/tokens.json
  (single source of truth per docs/BRAND.md) as CSS custom properties

Output:
- Gallery index page with client-side filtering by category and
  maturity (vanilla JS, no dependencies)
- One detail page per example with badges, feature tags, README
  content (links rewritten to GitHub URLs), and preview image
- Preview images copied when present; <img> gracefully omitted when
  absent
- Cougr Verified badge rendered when catalog provides verified field

CI integration:
- Generator runs after sync.py verification, before mdbook build
- Workflow triggers on catalog.toml, example READMEs, preview SVGs,
  and tokens.json changes in addition to existing paths

Closes salazarsebas#300
@drips-wave

drips-wave Bot commented Aug 25, 2026

Copy link
Copy Markdown

@sanusishafii989 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@salazarsebas salazarsebas left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Really nice generator overall — the HTML escaping is consistent everywhere I checked (titles, descriptions, badges, features), and the design-token consumption is a good call.

One real issue: copy_previews() and check_preview() join the preview / screenshot fields from catalog.toml straight into a filesystem path with os.path.join(EXAMPLES_DIR, name, preview), with no validation. os.path.join discards earlier segments when the last one is absolute, and ../ segments aren't blocked either. A catalog.toml entry like preview = "/etc/passwd" or preview = "../../../../some/secret" would get copied into the publicly built site during CI. Since catalog.toml is exactly the file external contributors touch when adding a new example, this is reachable, not theoretical.

Suggested fix: reject any preview/screenshot value that's absolute, contains .., or whose resolved real path falls outside EXAMPLES_DIR/<name>. Happy to re-review once that's in.

catalog.toml screenshot/preview fields are contributor-editable and were joined directly into filesystem paths without validation. A malicious entry like screenshot = /etc/passwd or ../../../some/secret would get copied into the publicly built site during CI.

Add validate_preview_path() which rejects absolute paths, paths containing .. segments, and any path whose resolved real path falls outside EXAMPLES_DIR/<name>. Applied in both check_preview() and copy_previews().

@sanusishafii989 sanusishafii989 left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the review! Fixed the path traversal issue — added validate_preview_path() that rejects absolute paths, .. segments, and any resolved path outside the example directory. Applied in both check_preview() and copy_previews(). Pushed in the latest commit on this PR.

@salazarsebas salazarsebas left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the quick turnaround — validate_preview_path() is solid: os.path.isabs + rejecting .. segments + os.path.realpath prefix-checked against EXAMPLES_DIR/name correctly closes the preview/screenshot vector I flagged, symlinks included.

One gap remains, same class of bug:

  1. The name field itself is never validated. It comes straight from meta.get("name", key) in main() and feeds os.path.join(EXAMPLES_DIR, name) for reads, and — more seriously — os.path.join(SHOWCASE_SRC, f"{name}.md") for the generated output file, completely outside validate_preview_path(). Because that function computes its own "safe root" from EXAMPLES_DIR/name, a malicious name (e.g. ../../../../tmp/evil) moves the safe root along with it and the check still passes — plus the unguarded f"{name}.md" write path is a separate arbitrary-file-write, independent of the function entirely. catalog.toml is exactly as contributor-editable as preview/screenshot, so name needs the same treatment (e.g. restrict to ^[a-zA-Z0-9_-]+$, or run the same realpath-prefix check against EXAMPLES_DIR itself).

Everything else here looks good — generator architecture, filters, detail pages, CI ordering, and token consumption all match #300. This is close.

Copy link
Copy Markdown
Owner

Addressed the remaining review point: catalog name is now validated the same way as preview/screenshot (^[a-zA-Z0-9_-]+$ plus a realpath-prefix check against EXAMPLES_DIR). Invalid names are skipped instead of being used as read/write paths.

@salazarsebas salazarsebas left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@salazarsebas
salazarsebas merged commit d07f2b6 into salazarsebas:main Aug 31, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(showcase): build the static gallery generator

2 participants