Astro 5 static site with Vue 3 interactive islands. Consumes template data from ../templates/ and deploys to templates.comfy.org via Vercel.
This site/ package is independent from the parent workflow_templates/ Nx monorepo.
When adding CI workflows, scripts, or configuration:
- All CI workflows MUST use
paths: ['site/**']triggers - All CI jobs MUST use
working-directory: site - All
pnpm-lock.yamlcache paths MUST referencesite/pnpm-lock.yaml - Do NOT modify parent monorepo configs (
nx.json, rootpackage.json, etc.) - Do NOT add workflows that trigger on changes outside
site/
# Install dependencies (required before any other command)
pnpm install# Start Astro dev server (localhost:4321)
pnpm run dev
# Preview production build locally
pnpm run preview# Full build (runs prebuild: sync + sync:tutorials + generate:ai + generate:previews)
pnpm run build
# Run Astro CLI directly
pnpm run astro# Sync ALL templates for all locales (200+ templates × 11 languages)
pnpm run sync
# Sync top 50 templates by usage (faster for development)
pnpm run sync -- --top-50
# Sync top N templates by usage
pnpm run sync -- --limit 100
# Sync English only (faster for development)
pnpm run sync:en-only
# Sync a specific locale
pnpm run sync:locale zh
pnpm run sync:locale ja
# Sync tutorials from docs repo (builds knowledge base for AI context)
pnpm run sync:tutorials# Full AI generation for all templates (requires OPENAI_API_KEY)
pnpm run generate:ai
# Test mode - process first template only
pnpm run generate:ai:test
# Generate for specific template only
pnpm run generate:ai:template
# Or with explicit template name:
pnpm run generate:ai -- --template <name>
# Skip AI calls (use placeholder content)
pnpm run generate:ai -- --skip-ai
# Force regenerate all (ignore cache)
pnpm run generate:ai:force
# Or:
pnpm run generate:ai -- --force
# Dry run - show what would be regenerated without making changes
pnpm run generate:ai:dry-run
# Or:
pnpm run generate:ai -- --dry-run
# Combined: test specific template without AI
pnpm run generate:ai -- --template <name> --skip-ai# View cache statistics (entries, size, model usage, timeline)
pnpm run cache:status
# Clear all cached AI content (requires --force or -f)
pnpm run cache:clear --force
# Or preview what would be deleted:
pnpm run cache:clear --dry-run# Generate preview images for templates
pnpm run generate:previews
# Generate OG (Open Graph) images for social sharing
pnpm run generate:og# Run E2E tests with Playwright
pnpm run test:e2e
# Run E2E tests in headed mode (visible browser)
pnpm run test:e2e:headed
# Run visual regression tests
pnpm run test:visual
# Update visual regression snapshots
pnpm run test:visual:update# Run ESLint (fails on warnings)
pnpm run lint
# Run ESLint with auto-fix
pnpm run lint:fix
# Format all files with Prettier
pnpm run format
# Check formatting without writing
pnpm run format:check# Validate sitemap structure and URLs (run after build)
pnpm run validate:sitemap
# Run SEO audit on built site (checks meta, OG, structured data)
pnpm run audit:seo
# Research competitor keywords (outputs to docs/competitor-analysis.md)
pnpm run research:competitors
# Research "People Also Ask" questions (outputs to docs/paa-research.md)
pnpm run research:paaThe prebuild script runs automatically before build and executes in parallel phases:
Phase 1 (parallel):
sync- Sync template metadatasync:tutorials- Sync tutorial content
Phase 2 (parallel):
generate:ai- Generate AI contentgenerate:previews- Generate preview imagesgenerate:og- Generate OG images
# Run full build with detailed timing breakdown
pnpm run build:profile
# Run sequential prebuild (for debugging)
pnpm run prebuild:sequentialsite/
├── scripts/
│ ├── generate-ai.ts # Main AI generation pipeline
│ └── sync-tutorials.ts # Syncs tutorials from docs repo
├── knowledge/
│ ├── prompts/
│ │ ├── system.md # Base system prompt
│ │ ├── tutorial.md # Tutorial content template
│ │ ├── showcase.md # Showcase content template
│ │ ├── comparison.md # Comparison content template
│ │ └── breakthrough.md # Breakthrough content template
│ ├── models/ # Model-specific documentation
│ ├── concepts/ # Domain concept documentation
│ └── tutorials/ # Synced tutorials from docs repo
├── overrides/templates/ # Human-edited content (preserved)
├── src/content/templates/ # Generated content (git-ignored)
└── .content-cache/ # AI generation cache (git-ignored)
When generating content, select appropriate template based on:
- tutorial: Default for most templates, step-by-step guides
- showcase: Templates with strong visual outputs
- comparison: Templates that compete with alternatives
- breakthrough: New model releases, cutting-edge features
- Content template selection: Automatically selects tutorial/showcase/comparison/breakthrough based on template metadata
- Tutorial context injection: Matches templates to relevant docs.comfy.org tutorials for better AI context
- Quality validation: Checks word count, step count, FAQ count, keyword presence, and meta description length
- Smart caching with versioning:
- Cache manifest tracks template hash, prompt version hash, generation timestamp, and model used
- Prompt changes automatically invalidate affected cache entries
--forceflag to regenerate all content--dry-runflag to preview what would be regenerated- Cache statistics output (hits, misses, regenerated)
generate-ai.ts- Main generation pipeline with CLI optionssync-tutorials.ts- Syncs tutorials from docs repo to knowledge basesync-templates.ts- Syncs template metadata (supports 11 languages)knowledge/prompts/system.md- Base GPT-4o system prompt../docs/ai-content-generation-strategy.md- Full strategy documentation
| Code | Language | Native Name | RTL |
|---|---|---|---|
| en | English | English | No |
| zh | Chinese (Simplified) | 简体中文 | No |
| zh-TW | Chinese (Traditional) | 繁體中文 | No |
| ja | Japanese | 日本語 | No |
| ko | Korean | 한국어 | No |
| es | Spanish | Español | No |
| fr | French | Français | No |
| ru | Russian | Русский | No |
| tr | Turkish | Türkçe | No |
| ar | Arabic | العربية | Yes |
| pt-BR | Portuguese (Brazil) | Português | No |
- English (default):
/workflows/,/workflows/{slug}/ - Other locales:
/{locale}/workflows/,/{locale}/workflows/{slug}/
src/i18n/config.ts- Language definitions, locale listsrc/i18n/utils.ts- URL localization, locale detectionsrc/i18n/ui.ts- UI string translationssrc/components/HreflangTags.astro- SEO hreflang tagssrc/lib/templates.ts- Template utilities with locale support
- Source data:
../templates/index.{lang}.json(12 files) - Sync script reads all locale files and writes to
src/content/templates/ - English templates at root, localized at
src/content/templates/{locale}/ - Pages generated at
/{locale}/workflows/via[locale]/templates/routes
| Variable | Required | Description |
|---|---|---|
OPENAI_API_KEY |
For AI gen | OpenAI API key |
SKIP_AI_GENERATION |
Optional | Set true for placeholder mode |
PUBLIC_HUB_API_URL |
Optional | Hub API base URL used by local builds and manual runs |
PUBLIC_COMFY_CLOUD_URL |
Optional | Comfy Cloud app URL used for CTA links |
The site code and build scripts still read PUBLIC_HUB_API_URL for hub API calls and
PUBLIC_COMFY_CLOUD_URL for CTA links.
- CI preview builds map
HUB_API_URL_PREVIEWtoPUBLIC_HUB_API_URL - CI production builds map
HUB_API_URL_PRODUCTIONtoPUBLIC_HUB_API_URL - CI preview builds map
COMFY_CLOUD_URL_PREVIEWtoPUBLIC_COMFY_CLOUD_URL - CI production builds map
COMFY_CLOUD_URL_PRODUCTIONtoPUBLIC_COMFY_CLOUD_URL - Local development should set these public env vars directly when a non-default backend is needed
Mirror the production values in Vercel Project Settings for PUBLIC_HUB_API_URL and
PUBLIC_COMFY_CLOUD_URL. cron-rebuild-site.yml triggers a Vercel deploy hook, so scheduled
rebuilds use Vercel-managed environment variables rather than the GitHub Actions secret mapping.
GitHub Actions workflows live at the repo root (.github/workflows/):
| Workflow | Trigger | Description |
|---|---|---|
lint-site.yml |
Push/PR to site/** |
Runs lint and format checks |
e2e-tests-site.yml |
Push/PR to site/** |
Runs Playwright E2E tests |
visual-regression-site.yml |
Push/PR to site/** |
Runs visual regression tests |
seo-audit-site.yml |
Push/PR to site/** |
Runs SEO audit on built site |
lighthouse.yml |
Push/PR to site/** |
Runs Lighthouse CI checks |
deploy-site.yml |
Manual dispatch | Deploys to Vercel production |
Preview deployments are handled automatically by Vercel on every PR.
Husky + lint-staged is configured for pre-commit hooks:
- JS/TS/Astro files: ESLint fix + Prettier
- JSON/MD/YAML/CSS files: Prettier
Run pnpm install to set up hooks (via prepare script).
docs/ai-content-generation-strategy.md- Content strategydocs/PRD.md- Product requirementsdocs/TDD.md- Technical designdocs/ROADMAP.md- AI content generation roadmapdocs/design-integration-guide.md- REQUIRED READING when implementing Figma designsdocs/seo-setup-guide.md- Search engine setup instructions
All Vue components MUST use standard Vue 3 Composition API and idiomatic Astro patterns. Write senior-level, production-quality code.
<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
src/composables/using module-level reactive refs - Template refs via
useTemplateRef()orref<HTMLElement | null>(null) - Lifecycle:
onMounted(),onUnmounted()— always clean up listeners
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 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)
Astro renders pages as static HTML at build time. Interactive sections use Vue 3 components mounted as islands via client:* directives. Each island is an independent Vue app instance — they do NOT share a Vue app context.
| Use case | Component type | Notes |
|---|---|---|
| Static content, layouts, SEO | .astro |
No client JS shipped |
| Interactive UI needed on page load | .vue + client:load |
Filters, search, drawers |
| Interactive UI below the fold | .vue + client:visible |
Hydrates when scrolled into view |
| SSR-only Vue (no interactivity) | .vue without client:* |
Renders at build, zero client JS |
Astro pages own data fetching. Serialize content collection entries to plain JSON-compatible objects before passing as props:
---
const templates = await getCollection('templates');
const serialized = templates.map((t) => ({
name: t.data.name,
title: t.data.title,
// ... only plain values: string, number, boolean, arrays, plain objects
}));
---
<MyVueIsland client:load templates={serialized} locale={locale} />Vue islands receive data via defineProps<T>(). Never pass Date, Map, Set, class instances, or functions — only JSON-serializable data.
provide/inject, $emit, and $parent do NOT work across islands (separate Vue apps). Use shared composables with module-level reactive state in src/composables/:
// src/composables/useHubStore.ts
import { ref } from 'vue';
const mobileDrawerOpen = ref(false); // module-level singleton
const searchFocusTrigger = ref(0);
export function useHubStore() {
return {
mobileDrawerOpen,
searchFocusTrigger,
toggleMobileDrawer() {
mobileDrawerOpen.value = !mobileDrawerOpen.value;
},
requestSearchFocus() {
searchFocusTrigger.value++;
},
};
}Both islands import the same composable → share the same refs → fully reactive:
// Island A (HubBrowse.vue)
const store = useHubStore();
store.requestSearchFocus(); // triggers watcher in Island B
// Island B (SearchPopover.vue)
const store = useHubStore();
watch(
() => store.searchFocusTrigger.value,
() => {
inputRef.value?.focus();
}
);When a DOM element in .astro markup (e.g. a hamburger button rendered server-side) needs to trigger Vue state, the Vue island owns the listener — not the Astro file:
// In the Vue island's <script setup>
const store = useHubStore();
onMounted(() => {
document.getElementById('astro-button-id')?.addEventListener('click', store.someAction);
});
onUnmounted(() => {
document.getElementById('astro-button-id')?.removeEventListener('click', store.someAction);
});Do NOT add inline <script> tags in .astro files that dispatchEvent(new CustomEvent(...)). The Vue island is responsible for bridging to any Astro-rendered DOM elements.
Components within the same island (parent → child Vue components) use normal Vue patterns:
- Props down:
defineProps<T>() - Events up:
defineEmits<T>()+@eventin parent template - Provide/inject: works within the same island's component tree
- Composables: for shared logic within the island
When implementing designs from Figma, do NOT remove or modify these critical components:
- SEO:
SEOHead.astro,HreflangTags.astro,structuredDataprop - i18n:
t()function calls,localizeUrl(),lang/dirattributes - Telemetry:
<Analytics />, vitals script
See docs/design-integration-guide.md for the complete checklist.
Astro Config (astro.config.mjs):
- Enabled
build.concurrencyusing all available CPU cores for parallel page generation - Configured Sharp image service with pixel limits to prevent memory issues
- Enabled
experimental.responsiveImagesfor automatic srcset generation - Added Vite optimizations: manual chunking, dependency pre-bundling, disabled dev sourcemaps
Parallel Prebuild (scripts/prebuild-parallel.ts):
- Phase 1 runs
sync+sync:tutorialsin parallel (independent data sources) - Phase 2 runs
generate:ai+generate:previewsin parallel - Outputs timing breakdown and time saved vs sequential execution
Preview Generation (scripts/generate-previews.ts):
- Uses parallel workers based on CPU count (
os.cpus().length - 1) - Incremental generation: only regenerates when workflow file is newer than preview
- Outputs timing summary
Build Profiling (pnpm run build:profile):
- Runs full build pipeline with detailed timing for each phase
- Reports output file count and size
- Useful for identifying bottlenecks
Caching:
- AI content cached in
.content-cache/with hash-based invalidation - Preview images regenerated only when source workflow changes
- Astro's built-in caching for content collections
Tips for faster builds:
- Use
--skip-aiflag during development if AI content isn't needed - Run
pnpm run build:profileto identify slow phases - Preview generation is I/O bound; consider SSD or ramdisk for large sites