Compare commits

...

15 Commits

Author SHA1 Message Date
Michael B
c607b98130 fix(website): omit aria-label on hero video when no label provided
Drop the empty-string default on videoAriaLabel; bind aria-label only
when truthy and set aria-hidden="true" otherwise so the video is treated
as decorative instead of carrying an empty accessible name.
2026-06-05 16:09:30 -04:00
Michael B
9e9d08febf fix(website): tighten vertical spacing on models and AI models sections 2026-06-05 16:09:30 -04:00
Michael B
411de899a5 feat(website): rename grok-image to grok-imagine with 301 redirect
Updates API_PROVIDER_MAP and adds a LEGACY_SLUG_REDIRECTS mechanism so
old supported-models URLs 301 to their canonical slug via the existing
canonicalSlug field.
2026-06-05 16:09:30 -04:00
Michael B
4db6d81804 feat(website): allow per-item object-fit/position on GalleryCard
Adds optional objectFit and objectPosition fields to GalleryItem so callers can override how each media tile is fit/anchored within its frame. Anchors Amber Passage to the bottom so the fox stays in view rather than getting cropped at the top of the cover region.
2026-06-05 16:09:30 -04:00
Michael B
58ad02db4e feat(website): replace models hero image with autoplay video
Swap the static gallery.webp image in ModelsHeroSection for a muted,
looping, playsinline video so the hero better reflects the generative
nature of the supported models. Rename mediaSrc/mediaAlt to
videoSrc/videoAriaLabel and update both en and zh-CN models pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 16:05:11 -04:00
Michael B
e0c9c96052 MAke the CTA the same size as the other CTA on the page 2026-06-05 16:05:11 -04:00
Michael B
0b2c250c85 adjust models page spacing and unify AI models CTA label
The changes:
  - Tighten top padding on mobile for ModelsHeroSection and ModelCreationsSection (pt-16 lg:pt-36)
  - Add mobile bottom padding to the gallery card stack
  - Use the mobile CTA label at all breakpoints in AIModelsSection (desktop variant commented out)
2026-06-05 16:05:11 -04:00
Michael B
676226e3ad fix(website): translate models hero alt text to Chinese on zh-CN page
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 16:05:11 -04:00
Michael B
b2176e441a chore: remove unused models showcase components
ModelsShowcaseSection and ShowcaseCard were superseded by AIModelsSection
on the models page and are no longer referenced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 16:05:10 -04:00
Michael B
f15ef81f03 Trigger re-deploy 2026-06-05 16:05:10 -04:00
Michael B
a98d75dbee feat: use shared AIModelsSection on models page
Swap ModelsShowcaseSection for the shared product/shared/AIModelsSection
on both the EN and zh-CN models pages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-05 16:05:10 -04:00
Michael B
36c7fbfee0 refactor: extract ShowcaseCard component from models showcase
Move per-card markup out of ModelsShowcaseSection into a dedicated
ShowcaseCard component (mirroring GalleryCard). Switch the card to a
uniform h-80 height, animate the badge color on hover, and drop the
now-conflicting aspect-ratio classes that caused cards to overflow
their grid columns.
2026-06-05 16:05:10 -04:00
Michael B
3417fbb7e5 feat: add featured AI models showcase to models page 2026-06-05 16:05:10 -04:00
Michael B
6ed81e05db feat: add creations and product showcase sections to models page 2026-06-05 16:05:10 -04:00
Michael B
08354d74a3 feat: add models page hero section
Adds ModelsHeroSection component, models list pages (en and zh-CN),
and accompanying i18n translations.
2026-06-05 16:05:10 -04:00
11 changed files with 1865 additions and 500 deletions

View File

@@ -58,7 +58,7 @@ const API_PROVIDER_MAP: Record<string, { name: string; slug: string }> = {
runway: { name: 'Runway', slug: 'runway' },
vidu: { name: 'Vidu', slug: 'vidu' },
bfl: { name: 'Flux (API)', slug: 'flux-api' },
grok: { name: 'Grok Image', slug: 'grok-image' },
grok: { name: 'Grok Imagine', slug: 'grok-imagine' },
stability: { name: 'Stability AI', slug: 'stability-ai' },
bytedance: { name: 'Seedance (ByteDance)', slug: 'seedance-bytedance' },
bytedace: { name: 'Seedance (ByteDance)', slug: 'seedance-bytedance' },
@@ -86,6 +86,20 @@ const API_PROVIDER_MAP: Record<string, { name: string; slug: string }> = {
wavespped: { name: 'Wavespeed', slug: 'wavespeed' }
}
// Stub entries that exist only to issue 301 redirects from old slugs to
// their new canonical slugs. Keeps renames reproducible across regenerations.
const LEGACY_SLUG_REDIRECTS: OutputModel[] = [
{
slug: 'grok-image',
canonicalSlug: 'grok-imagine',
name: 'Grok Image',
displayName: 'Grok Image',
directory: 'partner_nodes',
huggingFaceUrl: '',
workflowCount: 0
}
]
function stripExt(name: string): string {
return name.replace(/\.(safetensors|ckpt|pt|bin)$/, '')
}
@@ -299,7 +313,8 @@ function run(): void {
throw new Error(
`Failed to parse ${file}: ${
error instanceof Error ? error.message : String(error)
}`
}`,
{ cause: error }
)
}
}
@@ -367,7 +382,7 @@ function run(): void {
displayName: m.name
}))
const combined = [...apiOutput, ...output]
const combined = [...apiOutput, ...output, ...LEGACY_SLUG_REDIRECTS]
const withThumbs = combined.filter((m) => m.thumbnailUrl).length
process.stdout.write(

View File

@@ -7,12 +7,16 @@ const {
item,
locale = 'en',
aspect = 'var(--aspect-ratio-gallery-card)',
mobile = false
mobile = false,
objectPosition = 'center',
objectFit = 'cover'
} = defineProps<{
item: GalleryItem
locale?: Locale
aspect?: string
mobile?: boolean
objectPosition?: string
objectFit?: string
}>()
defineEmits<{ click: [] }>()
@@ -31,13 +35,15 @@ defineEmits<{ click: [] }>()
loop
muted
playsinline
class="size-full object-cover transition-transform duration-300 group-hover:scale-105"
class="size-full transition-transform duration-300 group-hover:scale-105"
:style="{ objectPosition, objectFit }"
/>
<img
v-else
:src="item.image"
:alt="item.title"
class="size-full object-cover transition-transform duration-300 group-hover:scale-105"
class="size-full transition-transform duration-300 group-hover:scale-105"
:style="{ objectPosition, objectFit }"
/>
<!-- Desktop hover overlay -->
<div

View File

@@ -0,0 +1,152 @@
<script setup lang="ts">
import { ref } from 'vue'
import type { GalleryItem } from '../../data/gallery'
import type { Locale } from '../../i18n/translations'
import { t } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
import GalleryCard from '../gallery/GalleryCard.vue'
import GalleryDetailModal from '../gallery/GalleryDetailModal.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const modelName = 'Grok'
const ctaHref = 'https://comfy.org/workflows/model/grok'
const items: GalleryItem[] = [
{
id: 'subway-swan',
image: 'https://media.comfy.org/website/gallery/subway-swan_compressed.png',
title: 'Subway Swan',
userAlias: 'Purz Beats',
teamAlias: 'Comfy',
tool: 'Grok Imagine',
href: 'https://www.youtube.com/@PurzBeats'
},
{
id: 'milos-little-wonder',
video:
'https://media.comfy.org/website/gallery/milos-little-wonder_compressed.mp4',
title: 'Milos Little Wonder',
userAlias: 'Purz Beats',
teamAlias: 'Comfy',
tool: 'Grok Imagine',
href: 'https://www.youtube.com/@PurzBeats'
},
{
id: 'amber-passage',
image:
'https://media.comfy.org/website/gallery/amber-passage_compressed.jpg',
title: 'Amber Passage',
userAlias: 'Purz Beats',
teamAlias: 'Comfy',
tool: 'Grok Imagine',
href: 'https://www.youtube.com/@PurzBeats',
objectPosition: 'bottom'
},
{
id: 'neon-revenant',
video:
'https://media.comfy.org/website/gallery/neon-revenant_compressed.mp4',
title: 'Neon Revenant',
userAlias: 'Eric Solorio',
teamAlias: 'Comfy',
tool: 'Grok Imagine',
href: 'https://www.instagram.com/enigmatic_e'
},
{
id: 'midnight-umami',
image:
'https://media.comfy.org/website/gallery/midnight_umami_compressed.png',
title: 'Midnight Umami',
userAlias: 'Purz Beats',
teamAlias: 'Comfy',
tool: 'Grok Imagine',
href: 'https://www.youtube.com/@PurzBeats'
}
]
const modalOpen = ref(false)
const modalIndex = ref(0)
function openDetail(index: number) {
modalIndex.value = index
modalOpen.value = true
}
const title = t('models.list.creations.title', locale).replace(
'{name}',
modelName
)
const ctaLabel = t('models.list.creations.cta', locale)
</script>
<template>
<section
data-testid="model-creations"
class="flex flex-col items-center px-4 py-16 lg:px-20 lg:pt-36"
>
<h2
class="max-w-4xl text-center text-3xl font-light tracking-tight text-primary-comfy-canvas lg:text-5xl"
>
{{ title }}
</h2>
<BrandButton
:href="ctaHref"
variant="solid"
size="lg"
class="mt-16 px-8 py-4 uppercase"
>
{{ ctaLabel }}
</BrandButton>
<div class="mt-20 hidden w-full flex-col gap-2 lg:flex">
<div class="grid grid-cols-2 gap-2">
<GalleryCard
v-for="(item, i) in items.slice(0, 2)"
:key="i"
:item
:locale
:object-position="item.objectPosition"
:object-fit="item.objectFit"
@click="openDetail(i)"
/>
</div>
<div v-if="items.length > 2" class="grid grid-cols-3 gap-2">
<GalleryCard
v-for="(item, i) in items.slice(2, 5)"
:key="i + 2"
:item
:locale
:object-position="item.objectPosition"
:object-fit="item.objectFit"
@click="openDetail(i + 2)"
/>
</div>
</div>
<div
class="rounded-5xl bg-transparency-white-t4 mt-12 flex w-full flex-col gap-6 p-2 max-lg:pb-6 lg:hidden"
>
<GalleryCard
v-for="(item, i) in items"
:key="i"
:item
:locale
:object-position="item.objectPosition"
:object-fit="item.objectFit"
mobile
@click="openDetail(i)"
/>
</div>
<GalleryDetailModal
v-if="modalOpen"
:items
:initial-index="modalIndex"
:locale
@close="modalOpen = false"
/>
</section>
</template>

View File

@@ -0,0 +1,56 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import { t } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
const {
locale = 'en',
modelName,
ctaHref,
videoSrc,
videoAriaLabel
} = defineProps<{
locale?: Locale
modelName: string
ctaHref: string
videoSrc: string
videoAriaLabel?: string
}>()
</script>
<template>
<section class="flex flex-col items-center px-6 pt-16 text-center lg:pt-36">
<h1
class="max-w-4xl text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
>
{{ modelName }} in <span class="text-primary-comfy-yellow">ComfyUI</span>
</h1>
<p
class="mt-6 max-w-2xl text-sm text-pretty text-primary-comfy-canvas lg:text-base"
>
{{ t('hero.subtitle', locale) }}
</p>
<BrandButton
:href="ctaHref"
variant="solid"
size="lg"
class="mt-10 px-8 py-4 uppercase"
>
{{ t('models.list.heroCta', locale).replace('{name}', modelName) }}
</BrandButton>
<div class="mt-16 w-full max-w-5xl">
<video
:src="videoSrc"
:aria-label="videoAriaLabel || undefined"
:aria-hidden="videoAriaLabel ? undefined : true"
autoplay
loop
muted
playsinline
preload="metadata"
class="rounded-4.5xl size-full object-cover"
/>
</div>
</section>
</template>

View File

@@ -78,7 +78,7 @@ function getCardClass(layoutClass: string): string {
<template>
<section
class="bg-primary-comfy-ink max-w-9xl mx-auto px-4 py-24 lg:px-20 lg:py-40"
class="max-w-9xl mx-auto bg-primary-comfy-ink px-4 py-16 lg:px-20 lg:py-40"
>
<div class="mx-auto flex w-full max-w-7xl flex-col items-center">
<p
@@ -88,18 +88,18 @@ function getCardClass(layoutClass: string): string {
</p>
<h2
class="text-primary-comfy-canvas text-3.5xl/tight mt-8 max-w-4xl text-center font-light lg:text-5xl"
class="text-3.5xl/tight mt-8 max-w-4xl text-center font-light text-primary-comfy-canvas lg:text-5xl"
>
{{ t('cloud.aiModels.heading', locale) }}
</h2>
<p
class="text-primary-comfy-canvas mt-8 max-w-xl text-center text-sm font-light lg:text-base/snug"
class="mt-8 max-w-xl text-center text-sm font-light text-primary-comfy-canvas lg:text-base/snug"
>
{{ t('cloud.aiModels.subtitle', locale) }}
</p>
<div class="mt-24 w-full">
<div class="mt-16 w-full lg:mt-24">
<div class="rounded-4xl border border-white/12 p-2 lg:p-1.5">
<div class="grid grid-cols-1 gap-2 lg:grid-cols-12">
<a
@@ -180,14 +180,15 @@ function getCardClass(layoutClass: string): string {
<BrandButton
:href="externalLinks.workflows"
variant="outline"
class="mt-4 w-full max-w-md text-center lg:mt-8 lg:w-auto"
size="lg"
class="mt-4 w-full max-w-md px-8 py-4 text-center lg:mt-8 lg:w-auto"
>
<span class="lg:hidden">{{
t('cloud.aiModels.ctaMobile', locale)
}}</span>
<span class="hidden lg:inline">{{
<!-- <span class="lg:hidden"> -->
{{ t('cloud.aiModels.ctaMobile', locale) }}
<!-- </span> -->
<!-- <span class="hidden lg:inline">{{
t('cloud.aiModels.ctaDesktop', locale)
}}</span>
}}</span> -->
</BrandButton>
</div>
</section>

File diff suppressed because it is too large Load Diff

View File

@@ -90,7 +90,7 @@ export const modelMetadata: Record<string, ModelOverride> = {
hubSlug: 'seedance',
featured: true
},
'grok-image': {
'grok-imagine': {
hubSlug: 'grok',
featured: false
},

View File

@@ -7,6 +7,8 @@ export interface GalleryItem {
teamAlias: string
tool: string
href?: string
objectPosition?: string
objectFit?: string
/** Defaults to true. Set to false to hide this item from rendered lists. */
visible?: boolean
}

View File

@@ -4458,6 +4458,76 @@ const translations = {
'zh-CN': '支持的模型'
},
// Models list page (/models)
'models.list.label': { en: 'MODELS', 'zh-CN': '模型' },
'models.list.heroCta': {
en: 'Try {name} Now',
'zh-CN': '立即试用 {name}'
},
'models.list.creations.title': {
en: '{name} Image and Video Creations',
'zh-CN': '{name} 图像与视频创作'
},
'models.list.creations.cta': {
en: 'Explore Workflows',
'zh-CN': '探索工作流'
},
'models.list.heroTitle.before': {
en: 'Run the worlds leading AI models in',
'zh-CN': '在以下平台运行世界领先的 AI 模型'
},
'models.list.heroSubtitle': {
en: 'From open-source diffusion checkpoints to partner APIs — every major model, with community workflow templates ready to run.',
'zh-CN':
'从开源扩散模型到合作伙伴 API涵盖每一个主流模型并附带可直接运行的社区工作流模板。'
},
'models.list.card.workflows': {
en: '{count} workflows',
'zh-CN': '{count} 个工作流'
},
'models.list.contact.label': {
en: 'COMFY HUB',
'zh-CN': 'COMFY HUB'
},
'models.showcase.label': { en: 'AI MODELS', 'zh-CN': 'AI 模型' },
'models.showcase.heading': {
en: 'Run the worlds\nleading AI models',
'zh-CN': '运行全球领先的\nAI 模型'
},
'models.showcase.subtitle': {
en: 'New models are added as they launch.',
'zh-CN': '新模型发布后会第一时间上线。'
},
'models.showcase.cta': {
en: 'EXPLORE WORKFLOWS',
'zh-CN': '探索工作流'
},
'models.showcase.card.grokImagine': {
en: 'Grok Imagine',
'zh-CN': 'Grok Imagine'
},
'models.showcase.card.nanoBananaPro': {
en: 'Nano Banana Pro',
'zh-CN': 'Nano Banana Pro'
},
'models.showcase.card.ltx23': {
en: 'LTX 2.3',
'zh-CN': 'LTX 2.3'
},
'models.showcase.card.qwenAdvancedEdit': {
en: 'Advanced image\nediting with Qwen',
'zh-CN': '使用 Qwen 进行\n高级图像编辑'
},
'models.showcase.card.wan22TextToVideo': {
en: 'Wan 2.2\ntext to video',
'zh-CN': 'Wan 2.2\n文字转视频'
},
'models.list.contact.heading': {
en: 'Pick a model and explore what the community has built. <a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">Browse Comfy Hub</a> for the newest workflows.',
'zh-CN':
'选择一个模型,浏览社区的创作成果。<a href="https://comfy.org/workflows" target="_blank" rel="noopener noreferrer" class="text-primary-comfy-yellow underline">访问 Comfy Hub</a> 查看最新工作流。'
},
// Payment status pages
'payment.success.label': {
en: 'PAYMENT',

View File

@@ -0,0 +1,22 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import ModelsHeroSection from '../components/models/ModelsHeroSection.vue'
import ModelCreationsSection from '../components/models/ModelCreationsSection.vue'
import AIModelsSection from '../components/product/shared/AIModelsSection.vue'
import ProductShowcaseSection from '../components/home/ProductShowcaseSection.vue'
---
<BaseLayout
title="Models — Comfy"
description="Run the world's leading AI models in ComfyUI. Browse every supported model with community workflow templates ready to run."
>
<ModelsHeroSection
modelName="Grok Imagine"
ctaHref="/p/supported-models/grok-imagine"
videoSrc="https://media.comfy.org/website/models/video_ComfdyUI_00001_.mp4"
videoAriaLabel="Grok Imagine output created with ComfyUI"
/>
<ModelCreationsSection client:load />
<AIModelsSection client:load />
<ProductShowcaseSection client:load />
</BaseLayout>

View File

@@ -0,0 +1,23 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import ModelsHeroSection from '../../components/models/ModelsHeroSection.vue'
import ModelCreationsSection from '../../components/models/ModelCreationsSection.vue'
import AIModelsSection from '../../components/product/shared/AIModelsSection.vue'
import ProductShowcaseSection from '../../components/home/ProductShowcaseSection.vue'
---
<BaseLayout
title="模型 — Comfy"
description="在 ComfyUI 中运行世界领先的 AI 模型。浏览所有支持的模型及社区工作流模板。"
>
<ModelsHeroSection
locale="zh-CN"
modelName="Grok Imagine"
ctaHref="/zh-CN/p/supported-models/grok-imagine"
videoSrc="https://media.comfy.org/website/models/video_ComfdyUI_00001_.mp4"
videoAriaLabel="使用 ComfyUI 创建的 Grok Imagine 作品"
/>
<ModelCreationsSection client:load locale="zh-CN" />
<AIModelsSection client:load locale="zh-CN" />
<ProductShowcaseSection client:load locale="zh-CN" />
</BaseLayout>