## Summary - Adds programmatic SEO model pages at `/p/supported-models/[slug]` for **207 models** auto-generated from `workflow_templates` (180 local + 27 partner nodes) - 3-file architecture: `generated-models.json` (auto-generated, checked in) + `model-metadata.ts` (editorial overrides) + `models.ts` (65-line merger) - Full JSON-LD per page: `SoftwareApplication` + `BreadcrumbList` + `FAQPage` (targeting AI Overviews / People Also Ask) - Partner node support: `directory: 'partner_nodes'` hides Download button, shows VIEW TUTORIAL - `generate-models.ts`: walks `workflow_templates` for local models + `API_PROVIDER_MAP` for 30+ partner integrations (Kling, Meshy, Luma, Runway, Stability AI, ByteDance, Google, etc.) - Weekly GH Actions workflow opens issue when new models appear in `workflow_templates` but not in `generated-models.json` - `add-model-page` Claude skill for Slack-driven model page PRs ## Files changed | File | Purpose | |------|---------| | `apps/website/src/config/generated-models.json` | Auto-generated, 207 models (27 partner + 180 local) | | `apps/website/src/config/model-metadata.ts` | Editorial overrides: docsUrl, blogUrl, featured (9 entries) | | `apps/website/src/config/models.ts` | 65-line merger — imports JSON + overrides, exports `models` + `getModelBySlug` | | `apps/website/scripts/generate-models.ts` | Build-time parser; run with `pnpm generate:models` | | `apps/website/src/i18n/translations.ts` | ~30 UI keys added (no per-model keys — displayName is plain string) | | `apps/website/src/pages/p/supported-models/[slug].astro` | Dynamic route with 3x JSON-LD schemas | | `apps/website/src/pages/p/supported-models/index.astro` | Model grid index page | | `apps/website/src/components/models/ModelHeroSection.vue` | Hero component | | `.github/workflows/model-page-discovery.yaml` | Weekly auto-discovery workflow | | `.claude/skills/add-model-page/SKILL.md` | Claude skill for adding/updating model pages | ## Test plan - [ ] `pnpm build` passes in `apps/website` - [ ] `/p/supported-models` index renders 207 model cards - [ ] `/p/supported-models/kling-ai` shows Partner Node eyebrow, no Download button, VIEW TUTORIAL CTA - [ ] `/p/supported-models/flux-1-dev` shows Diffusion Model eyebrow, Download + Tutorial buttons - [ ] `/p/supported-models/umt5-xxl-fp8-e4m3fn-scaled` redirects 301 to `umt5-xxl-fp16` (canonicalSlug) - [ ] Structured data validator shows FAQPage + SoftwareApplication + BreadcrumbList valid Fixes FE-421 --------- Co-authored-by: GitHub Action <action@github.com> Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Website Scripts
refresh-ashby-snapshot.ts
Pulls the latest job postings from Ashby and writes
src/data/ashby-roles.snapshot.json. Invoked by the Release: Website
GitHub Actions workflow; also runnable locally via
pnpm --filter @comfyorg/website ashby:refresh-snapshot.
process-videos.sh
Generates multi-resolution VP9/WebM + H.264/MP4 variants and a poster
frame for marketing videos using ffmpeg. Run locally before
uploading the outputs to media.comfy.org; this is not wired into CI.
apps/website/scripts/process-videos.sh \
./video-sources \
./dist/videos \
"640 960 1280 1920"
Output
For each source video at ./video-sources/foo.mp4, you get:
foo-640.webm foo-640.mp4
foo-960.webm foo-960.mp4
foo-1280.webm foo-1280.mp4
foo-1920.webm foo-1920.mp4
foo-poster.jpg
The naming convention is enforced by buildVideoSources() in
src/utils/video.ts, which the <SiteVideo> Vue component uses to
emit <source> URLs.
Pairing with <SiteVideo>
Once the assets are uploaded, render them with:
<SiteVideo
name="foo"
base-url="https://media.comfy.org/website/marketing"
:width="1280"
:formats="['webm', 'mp4']"
poster="https://media.comfy.org/website/marketing/foo-poster.jpg"
autoplay
loop
/>
<SiteVideo> vs <VideoPlayer>
SiteVideo— lightweight multi-source<video>for decorative or autoplay marketing clips. No custom controls, no captions UI.VideoPlayer— full-featured player with custom scrubber, mute, fullscreen, and caption toggles. Use this for content with subtitles or user-driven playback.
If you need both responsive sources and the rich VideoPlayer chrome, the
two are not yet combined; either pick one or extend VideoPlayer to accept
a source list.
Encoder choices
- VP9/WebM at CRF 32 — preferred by Chrome and Firefox; smaller files.
- H.264/MP4 at CRF 23, High profile,
+faststart— universal fallback, required for Safari iOS. - Poster JPG at q4 — extracted from t=1s when the clip is long enough,
otherwise t=0; scaled to 1280w. Use this as the
posterattribute so the video shows something while loading.
Why a single resolution per video
<source media="..."> inside <video> is unreliable across browsers
(Safari ignores it). The simplest correct strategy is to ship one
well-sized resolution and let CSS scale it down on smaller viewports.
The script generates multiple widths so you can pick a different one
per page (e.g. 1280w for a hero, 640w for a thumbnail), or wire up
JavaScript-based selection later if metrics demand it.