A monorepo managing ComfyUI workflow templates distributed as Python packages AND a public Astro-based workflow hub website at templates.comfy.org. Two distinct systems share the same template data.
workflow_templates/
├── templates/ # SOURCE OF TRUTH: workflow JSONs, thumbnails, index metadata
│ ├── index.json # Master template metadata (English)
│ ├── index.{locale}.json # 11 locale variants (zh, ja, ko, es, fr, ru, tr, ar, pt-BR, zh-TW)
│ ├── *.json # ComfyUI workflow definitions
│ └── *-1.webp, *-2.webp # Template thumbnails
├── blueprints/ # Reusable subgraph blueprint definitions
├── bundles.json # Maps template names → Python package bundles
├── blueprints_bundles.json # Maps blueprint names → package
├── packages/ # Python distribution packages (Nx monorepo)
│ ├── core/ # Loader + manifest
│ ├── media_api/ # API-calling templates (Replicate, BFL, etc.)
│ ├── media_image/ # Image generation templates
│ ├── media_video/ # Video generation templates
│ ├── media_other/ # Audio, 3D, utilities
│ ├── meta/ # Meta package aggregating all above
│ └── blueprints/ # Subgraph blueprints package
├── scripts/ # Python: validation, sync, i18n
├── site/ # INDEPENDENT Astro 5 project (see "Site" section below)
├── docs/ # Specs, i18n guide, publishing guide
├── .claude/skills/ # 6 Claude skill definitions
├── .github/workflows/ # CI/CD (validation, deploy, lint, tests)
├── pyproject.toml # Python project version & config
├── package.json # Nx monorepo root (npm run sync, etc.)
└── nx.json # Nx workspace config
- Templates are grouped into 4 media bundles via
bundles.json scripts/sync_bundles.pycopies templates + thumbnails into package directories- Published to PyPI as
comfyui-workflow-templates-*packages - Version lives in root
pyproject.toml(currently 0.8.43)
- Independent project — own
package.json,pnpm-lock.yaml, tooling - Consumes templates from
../templates/via sync scripts - AI content generation pipeline (GPT-4o) enriches template pages
- Deployed to Vercel
templates/index.json + *.json + *.webp
├──→ scripts/sync_bundles.py ──→ packages/media_*/
└──→ site/scripts/sync-templates.ts ──→ site/src/content/templates/
└──→ site/scripts/generate-ai.ts ──→ AI-enriched content
└──→ astro build ──→ templates.comfy.org (Vercel)
npm run sync # Sync bundle manifests + assets to packages
python scripts/validate_templates.py # Validate template JSON
python scripts/sync_data.py --templates-dir templates # Sync i18n translationspnpm install # Install deps (required first)
pnpm run dev # Dev server at localhost:4321
pnpm run build # Full build (prebuild + astro build)
pnpm run sync # Sync templates from ../templates/
pnpm run sync -- --top-50 # Sync top 50 only (faster dev)
pnpm run generate:ai # AI content generation (needs OPENAI_API_KEY)
pnpm run generate:ai -- --skip-ai # Use placeholder content (no API key needed)
pnpm run lint # ESLint
pnpm run format # Prettier
pnpm run test:e2e # Playwright E2E testsEach template in templates/index.json has:
name— Must match the JSON filename (snake_case, no extension)title,description— Display metadatamediaType— "image" | "video" | "audio" | "3d"mediaSubtype— Usually "webp"thumbnailVariant— "compareSlider" | "hoverDissolve" | "hoverZoom" | "zoomHover" | nulltags,models,logos,date,usage,size,vram,searchRanktutorialUrl,openSource,requiresCustomNodes,io
Standard ComfyUI workflow format with embedded model metadata:
properties.models[]— Download URLs, SHA256 hashes, target directoriesproperties.cnr_id+properties.ver— Node version pinning
- Named
{template_name}-1.webp(primary),{template_name}-2.webp(comparison) - WebP format, target <1MB, 512×512 or 768×768
Templates in bundles.json map to Python packages:
| Bundle | Contents |
|---|---|
media-api |
Templates using external APIs |
media-image |
Image generation/editing |
media-video |
Video generation |
media-other |
Audio, 3D, utilities |
en (default), zh, zh-TW, ja, ko, es, fr, ru, tr, ar, pt-BR
- Master:
templates/index.json(English) - Locales:
templates/index.{locale}.json - Translation tracking:
scripts/i18n.json - Sync:
python scripts/sync_data.py --templates-dir templates
- Config:
site/src/i18n/config.ts - UI strings:
site/src/i18n/ui.ts - URL pattern: English at
/templates/, others at/{locale}/templates/ - SEO: Hreflang tags via
HreflangTags.astro
site/src/pages/— Route pages ([slug].astro, [locale]/templates/)site/src/components/— Astro (.astro) and Vue (.vue) componentssite/src/composables/— Shared Vue 3 composables for cross-island statesite/src/lib/— Utilities (templates.ts, urls.ts, slugify.ts, model-logos.ts)site/src/content/— Content collections (git-ignored, generated by sync)site/scripts/— Build scripts (sync, AI generation, previews, OG images)site/knowledge/— AI generation context (prompts, model docs, concepts)site/overrides/templates/— Human-edited content (survives AI regeneration)
Astro renders pages as static HTML. Interactive sections use Vue 3 components mounted as islands via client:* directives. Each island is a separate Vue app instance.
When to use Astro vs Vue:
.astro— Static content, layouts, SEO markup, data fetching (getCollection(), API calls).vuewithclient:load— Interactive UI that needs reactivity on page load (filters, search, drawers).vuewithclient:visible— Interactive UI that can wait until scrolled into view (below-fold widgets).vuewithoutclient:*— SSR-only Vue (renders HTML at build time, no client JS)
Data flow — Astro page → Vue island:
[page].astro Vue island
───────────── ──────────
getCollection('templates')
→ serialize to plain objects
→ pass as props via client:load → defineProps<T>()
Always serialize Astro content collection entries to plain objects before passing to Vue. Vue islands cannot receive Astro class instances, Date objects, or Map/Set — only JSON-serializable data.
Cross-island communication — Vue island ↔ Vue island:
Each client:load creates a separate Vue app, so provide/inject and $emit do NOT work across islands. Use shared composables with module-level reactive state:
site/src/composables/useHubStore.ts (module-level refs)
├── HubBrowse.vue (island 1) imports useHubStore()
└── SearchPopover.vue (island 2) imports useHubStore(), watches shared ref
Module-level ref() values are singletons in the browser bundle — all islands that import the same composable share the same reactive state.
Astro → Vue runtime bridge:
When a DOM element in Astro markup (e.g. a hamburger button) needs to trigger Vue state, the Vue island attaches a listener to that element by ID in onMounted():
// In the Vue island's <script setup>
onMounted(() => {
document.getElementById('some-astro-button')
?.addEventListener('click', store.someAction);
});Do NOT use inline <script> tags in .astro files that dispatchEvent(new CustomEvent(...)). The Vue island owns the listener.
sync-templates.tssyncs metadata from../templates/generate-ai.tscalls GPT-4o with context fromknowledge/- Generates: extendedDescription, howToUse[], metaDescription, suggestedUseCases[], faqItems[]
- Content templates: tutorial (default), showcase, comparison, breakthrough
- Cached in
.content-cache/with hash-based invalidation - Human overrides in
site/overrides/templates/{name}.json(sethumanEdited: true)
SEOHead.astro— Meta tags, structured dataHreflangTags.astro— i18n SEOt()calls,localizeUrl()— i18n functions<Analytics />— Telemetry
validate-templates.yml— JSON schema validationvalidate-blueprints.yml— Blueprint validationvalidate-manifests.yml— Manifest sync checklink-checker.yml— Model download URL validation
lint-site.yml— ESLint + Prettiere2e-tests-site.yml— Playwright testsvisual-regression-site.yml— Visual regressionseo-audit-site.yml— SEO auditlighthouse.yml— Performance checksdeploy-site.yml— Manual Vercel deploy
- Python: Ruff, line-length 100, py312, rules E/F
- TypeScript/Astro: ESLint + Prettier (configured in site/)
- Templates: snake_case naming, JSON format
- Commits: Bump version in
pyproject.tomlwhen modifying templates
All Vue components MUST use standard Vue 3 Composition API and idiomatic Astro patterns. Write senior-level, production-quality code.
Vue 3 — Required Patterns:
<script setup lang="ts">for all components — no Options API- Standard reactivity:
ref(),computed(),watch(),watchEffect() - Props via
defineProps<T>(), emits viadefineEmits<T>() - Cross-component state via shared composables in
site/src/composables/using module-level reactive refs - Template refs via
useTemplateRef()orref<HTMLElement | null>(null) - Lifecycle:
onMounted(),onUnmounted()— never rawaddEventListeneronwindow/documentwithout cleanup
Vue 3 — Forbidden Patterns:
document.dispatchEvent(new CustomEvent(...))for component communication — use composablesdocument.addEventListener(...)to listen for custom events from other Vue components- Event bus libraries or mitt — use shared composables with reactive state instead
- Options API (
data(),methods,computed:,watch:as object) this.$emit,this.$refs, or anythis-based API- Mixins — use composables
Astro — Required Patterns:
- Astro components (
.astro) for static/SSR content, Vue islands (client:load/client:visible) for interactivity - Pass data from Astro to Vue via props only — serialize to plain objects
- For Astro-to-Vue runtime communication (e.g. a button in
.astrotriggering Vue state), attach event listeners to specific DOM elements by ID inside the Vue component'sonMounted()— do NOT use inline<script>tags withdispatchEvent - Cross-island state sharing via shared composables (module-level refs are singletons in the browser bundle)
/adding-templates— Add new workflow templates (full workflow)/managing-bundles— Move templates between bundles, reorder/managing-thumbnails— Add/replace/audit thumbnails/managing-translations— Sync/check translations across 11 languages/editing-site-content— Edit site page content with overrides/regenerating-ai-content— Regenerate AI descriptions, manage cache
docs/SPEC.md— Formal template JSON schemadocs/BLUEPRINTS.md— Subgraph blueprint specdocs/I18N_GUIDE.md— Translation management workflowsite/docs/PRD.md— Product requirements for the sitesite/docs/TDD.md— Technical design documentsite/docs/design-integration-guide.md— REQUIRED when implementing Figma designs