Skip to content

Commit 2359c39

Browse files
imick-ioclaude
andauthored
feat(website): add /fdct Forward Deployed Creative Technologists page (#14907)
## Summary Adds the new **Forward Deployed Creatives** marketing page in both locales — `/forward-deployed-creatives` and `/zh-CN/forward-deployed-creatives` — built from the Figma design ([desktop](https://www.figma.com/design/11vkE4FAn4plEYpawd57zS/Comfy----Website-Design?node-id=10268-33788), [mobile hero](https://www.figma.com/design/11vkE4FAn4plEYpawd57zS/Comfy----Website-Design?node-id=10373-39106)). The route is registered in `config/routes.ts` (`fdct`) and linked from the main nav (under **Programs**, with a `new` badge) and the site footer. **Page sections, in order:** 1. **Hero** — parallax collage of FDC work on desktop (`useParallax`, per-item scroll depth); collapses to a static stacked collage below `xl`. Single **Contact us** CTA → `/contact`. Decorative autoplay videos are paused under `prefers-reduced-motion`. 2. **Builders** — wire-node diagram (`WireNodeLayout`). 3. **Client logo marquee** (`SocialProofBarSection`). 4. **How it works** — numbered steps (`BenefitsGrid01`). 5. **What you get** — checklist (`ChecklistSplit01`). 6. **Featured technologists** — Doug Hogan, Chris V., Rob Losch, and Robert Paige ("Bert"), each with a bio dialog surfacing their real workflows. 7. **Featured projects** — tagged workflow cards linking to real published workflows on `comfy.org/workflows`. 8. **Closing CTA** — "Build with the people that build Comfy." → `/contact`. 9. **Q&A** — 5-item accordion, backed by a single data source (`fdctFaqs`) that also emits the `FAQPage` json-ld so structured data always matches on-page copy per locale. 10. **Enterprise CTA band** (`CtaBands01`) → `/contact`. ## New reusable blocks (each with a Storybook story) - `TeamGrid01` + `TeamMemberDialog01`, built on new `ui/dialog` primitives (`Dialog`, `DialogTrigger`, `DialogContent`, `DialogOverlay`, `DialogTitle`, `DialogDescription`, `DialogClose`). - `CtaBands01` — enterprise CTA band. - `CardWorkflow01` + `CardWorkflowGallery01` — tagged workflow cards for the featured-projects gallery. ## Backward-compatible block extensions `WireNodeLayout`, `BenefitsGrid01`, `ChecklistSplit01`, `HeroCentered01` (e.g. `eyebrowClass`), and `CardArticle01` / `CardArticleGallery01` gained opt-in props for this page — existing call sites are unaffected. Storybook stories were added for `BenefitsGrid01`, `ChecklistSplit01`, and `CardArticleGallery01`. ## Drafted copy needing review - **FAQ answers** (`fdct.faq.a1`–`a5`) are **drafted from the page's own claims**, not provided copy — please review the five Q&A pairs for accuracy of commitment. - All **zh-CN translations** in the `fdct.*` namespace are drafted. - Enterprise CTA band and closing-CTA copy are drafted. ## Test plan - [x] `e2e/fdct.spec.ts` — ~19 tests across three suites: `FDCT page @smoke` (en desktop), `FDCT hero @mobile`, and `FDCT page (zh-CN) @smoke`. Covers each section, the technologist bio dialog, Q&A expand, `FAQPage` json-ld pairs, and locale-prefixed CTA hrefs. - [x] `config/routes.test.ts` — canonical `/forward-deployed-creatives` for en and `/zh-CN/forward-deployed-creatives` for zh-CN. - [x] `pnpm --filter @comfyorg/website typecheck`, eslint, stylelint, oxfmt via pre-commit hooks. - [x] Visually matched against Figma at desktop and mobile widths in a real browser. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 4cb44e9 commit 2359c39

46 files changed

Lines changed: 3047 additions & 55 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/website/e2e/fdct.spec.ts

Lines changed: 422 additions & 0 deletions
Large diffs are not rendered by default.

apps/website/e2e/fixtures/blockExternalMedia.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const ANALYTICS_PATTERN = '**/va.vercel-scripts.com/**' as const
1414
const CDP_PATTERN = '**/cdp.customer.io/**' as const
1515
const YOUTUBE_EMBED_PATTERN = '**/*.youtube-nocookie.com/**' as const
1616
const MEDIA_PATTERN =
17-
/^https:\/\/media\.comfy\.org\/.*\.(webp|webm|mp4|png|jpg|jpeg|vtt)(\?.*)?$/i
17+
/^https:\/\/(media|comfy-hub-assets)\.comfy\.org\/.*\.(webp|webm|mp4|png|jpg|jpeg|vtt)(\?.*)?$/i
1818
const VIDEO_PATTERN = /\.(webm|mp4)(\?|$)/i
1919
const SUBTITLE_PATTERN = /\.vtt(\?|$)/i
2020

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
2+
3+
import BenefitsGrid01 from './BenefitsGrid01.vue'
4+
5+
const meta: Meta<typeof BenefitsGrid01> = {
6+
title: 'Website/Blocks/BenefitsGrid01',
7+
component: BenefitsGrid01,
8+
tags: ['autodocs'],
9+
args: {
10+
heading: 'Why join the program',
11+
benefits: [
12+
{ id: 'reach', description: 'Reach a global audience of builders.' },
13+
{ id: 'earn', description: 'Earn recurring commission on referrals.' },
14+
{ id: 'assets', description: 'Get ready-made creative assets.' },
15+
{ id: 'support', description: 'Dedicated partner support at every step.' }
16+
],
17+
primaryCta: { label: 'Apply now', href: '#' }
18+
}
19+
}
20+
21+
export default meta
22+
type Story = StoryObj<typeof meta>
23+
24+
export const Default: Story = {}
25+
26+
export const WithTitlesAndFootnote: Story = {
27+
args: {
28+
heading: 'How it works',
29+
benefits: [
30+
{
31+
id: 'validate',
32+
title: 'Validate',
33+
description: 'We prove the outcome is achievable on your own material.'
34+
},
35+
{
36+
id: 'build',
37+
title: 'Build',
38+
description:
39+
'We build the workflow inside your environment, on your models and infrastructure.'
40+
},
41+
{
42+
id: 'enable',
43+
title: 'Enable',
44+
description:
45+
'We train your team to run, modify, and extend it themselves.'
46+
},
47+
{
48+
id: 'own',
49+
title: 'Own',
50+
description:
51+
'It runs on your infrastructure, and your team can change it without us.'
52+
}
53+
],
54+
footnote: "The direct line stays open. We're here when your team needs us.",
55+
primaryCta: undefined
56+
}
57+
}

apps/website/src/components/blocks/BenefitsGrid01.vue

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script setup lang="ts">
2-
import BrandButton from '../common/BrandButton.vue'
32
import GlassCard from '../common/GlassCard.vue'
3+
import Button from '../ui/button/Button.vue'
4+
import { resolveRel } from '../../utils/cta'
45
5-
type Benefit = { id: string; description: string }
6+
type Benefit = { id: string; title?: string; description: string }
67
type Cta = {
78
label: string
89
href: string
@@ -12,6 +13,7 @@ type Cta = {
1213
defineProps<{
1314
heading: string
1415
benefits: readonly Benefit[]
16+
footnote?: string
1517
primaryCta?: Cta
1618
}>()
1719
</script>
@@ -36,25 +38,44 @@ defineProps<{
3638
>
3739
{{ String(index + 1).padStart(2, '0') }}
3840
</span>
41+
<h3
42+
v-if="benefit.title"
43+
class="text-2xl font-normal text-primary-warm-white"
44+
>
45+
{{ benefit.title }}
46+
</h3>
3947
<p
40-
class="text-base/relaxed font-medium text-primary-comfy-canvas lg:text-xl"
48+
:class="
49+
benefit.title
50+
? 'text-base/relaxed font-light text-primary-comfy-canvas'
51+
: 'text-base/relaxed font-medium text-primary-comfy-canvas lg:text-xl'
52+
"
4153
>
4254
{{ benefit.description }}
4355
</p>
4456
</article>
4557
</div>
4658
</GlassCard>
4759

60+
<p
61+
v-if="footnote"
62+
class="mt-10 text-center text-sm font-light text-primary-comfy-canvas lg:mt-12"
63+
>
64+
{{ footnote }}
65+
</p>
66+
4867
<div v-if="primaryCta" class="mt-10 flex justify-center lg:mt-12">
49-
<BrandButton
68+
<Button
69+
as="a"
5070
:href="primaryCta.href"
5171
:target="primaryCta.target"
52-
size="lg"
53-
class="px-20 py-4 text-base uppercase"
72+
:rel="resolveRel(primaryCta)"
5473
variant="outline"
74+
size="lg"
75+
class="px-20"
5576
>
5677
{{ primaryCta.label }}
57-
</BrandButton>
78+
</Button>
5879
</div>
5980
</section>
6081
</template>

apps/website/src/components/blocks/CardArticle01.vue

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import Badge from '../ui/badge/Badge.vue'
33
44
import { resolveRel } from '../../utils/cta'
5+
import CardArrow from '../common/CardArrow.vue'
56
import ButtonPill from '../ui/button-pill/ButtonPill.vue'
67
import Card from '../ui/card/Card.vue'
78
import CardContent from '../ui/card/CardContent.vue'
@@ -24,6 +25,7 @@ export type CardArticleItem = {
2425
title: string
2526
description?: string
2627
media: CardArticleMedia
28+
author?: { name: string; avatarSrc: string }
2729
cta: { label: string; href: string; newTab?: boolean }
2830
}
2931
@@ -34,7 +36,7 @@ const { item, titleClamp = false } = defineProps<{
3436
</script>
3537

3638
<template>
37-
<Card class="group/pill-trigger relative h-full overflow-hidden">
39+
<Card class="group group/pill-trigger relative h-full overflow-hidden">
3840
<a
3941
:href="item.cta.href"
4042
:target="item.cta.newTab ? '_blank' : undefined"
@@ -93,7 +95,28 @@ const { item, titleClamp = false } = defineProps<{
9395
</CardContent>
9496
</div>
9597

96-
<CardFooter class="mt-auto px-6 pb-6">
98+
<CardFooter
99+
v-if="item.author"
100+
class="mt-auto items-center justify-between gap-4 px-6 pb-6"
101+
>
102+
<span class="flex min-w-0 items-center gap-3">
103+
<img
104+
:src="item.author.avatarSrc"
105+
alt=""
106+
loading="lazy"
107+
decoding="async"
108+
class="size-8 shrink-0 rounded-full object-cover"
109+
/>
110+
<span class="truncate text-sm font-semibold text-primary-warm-white">
111+
{{ item.author.name }}
112+
</span>
113+
</span>
114+
<CardArrow
115+
hover="group"
116+
class="size-8 shrink-0 rounded-xl bg-primary-warm-gray text-primary-warm-white"
117+
/>
118+
</CardFooter>
119+
<CardFooter v-else class="mt-auto px-6 pb-6">
97120
<ButtonPill as="span" variant="ghost" icon-position="left">
98121
{{ item.cta.label }}
99122
</ButtonPill>

apps/website/src/components/blocks/CardArticleGallery01.stories.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,25 @@ export const WithTabsAndLoadMore: Story = {
6161
}
6262
}
6363

64+
export const ThreeColumnWithAuthors: Story = {
65+
args: {
66+
title: 'Past projects',
67+
titleAlign: 'center',
68+
layout: 'three-column',
69+
items: ['Advertisement', 'Entertainment', 'Ecommerce'].flatMap((category) =>
70+
[0, 1].map((row) => ({
71+
...item(`${category}-${row}`, 'Title of the project'),
72+
category,
73+
author: {
74+
name: 'Person McPersonface',
75+
avatarSrc: '/assets/images/fallback-gradient-avatar.svg'
76+
},
77+
cta: { label: 'View project', href: '#' }
78+
}))
79+
)
80+
}
81+
}
82+
6483
export const MixedLayout: Story = {
6584
args: {
6685
title: 'Latest Launches',

apps/website/src/components/blocks/CardArticleGallery01.vue

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const {
3131
title?: string
3232
titleAlign?: 'start' | 'center'
3333
items: CardArticleGalleryItem[]
34-
layout?: 'mixed' | 'two-column'
34+
layout?: 'mixed' | 'two-column' | 'three-column'
3535
titleClamp?: boolean
3636
tabs?: CardArticleGalleryTab[]
3737
allLabel?: string
@@ -52,7 +52,7 @@ const { visibleItems, hasMore, showMore } = useFilteredGallery({
5252
<section class="max-w-9xl mx-auto px-6 py-16 lg:py-24">
5353
<h2
5454
v-if="title && !tabs"
55-
class="text-primary-warm-white text-3xl font-light tracking-tight lg:text-5xl"
55+
class="text-3xl font-light tracking-tight text-primary-warm-white lg:text-5xl"
5656
:class="titleAlign === 'center' ? 'text-center' : undefined"
5757
>
5858
{{ title }}
@@ -67,7 +67,7 @@ const { visibleItems, hasMore, showMore } = useFilteredGallery({
6767
>
6868
<h2
6969
v-if="title"
70-
class="text-primary-warm-white text-3xl font-light tracking-tight lg:text-5xl"
70+
class="text-3xl font-light tracking-tight text-primary-warm-white lg:text-5xl"
7171
>
7272
{{ title }}
7373
</h2>
@@ -98,7 +98,13 @@ const { visibleItems, hasMore, showMore } = useFilteredGallery({
9898

9999
<div
100100
class="mt-10 grid grid-cols-1 gap-6 lg:mt-12"
101-
:class="layout === 'mixed' ? 'md:grid-cols-6' : 'md:grid-cols-2'"
101+
:class="
102+
layout === 'mixed'
103+
? 'md:grid-cols-6'
104+
: layout === 'three-column'
105+
? 'md:grid-cols-3'
106+
: 'md:grid-cols-2'
107+
"
102108
>
103109
<div
104110
v-for="(item, index) in visibleItems"
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import type { Meta, StoryObj } from '@storybook/vue3-vite'
2+
3+
import CardWorkflow01 from './CardWorkflow01.vue'
4+
5+
const meta: Meta<typeof CardWorkflow01> = {
6+
title: 'Website/Blocks/CardWorkflow01',
7+
component: CardWorkflow01,
8+
tags: ['autodocs'],
9+
args: {
10+
item: {
11+
id: 'product-advertisement-video',
12+
title: 'Product Advertisement Video',
13+
href: 'https://comfy.org/workflows/c98e5c457e1e-c98e5c457e1e/',
14+
media: {
15+
type: 'video',
16+
src: 'https://comfy-hub-assets.comfy.org/uploads/a8c26beb-d463-40a0-8547-fa942e53ad70.mp4',
17+
alt: 'Product Advertisement Video'
18+
},
19+
description:
20+
'Turn product stills into a polished advertisement spot, from generated shots to final cut.',
21+
tags: ['Image Generation', 'Image to Video']
22+
}
23+
},
24+
decorators: [
25+
() => ({
26+
template: '<div class="max-w-sm bg-primary-comfy-ink p-6"><story /></div>'
27+
})
28+
]
29+
}
30+
31+
export default meta
32+
type Story = StoryObj<typeof meta>
33+
34+
export const Default: Story = {}
35+
36+
export const ImageMedia: Story = {
37+
args: {
38+
item: {
39+
id: 'headphones-still',
40+
title: 'Product Still Workflow',
41+
href: 'https://comfy.org/workflows/c98e5c457e1e-c98e5c457e1e/',
42+
media: {
43+
type: 'image',
44+
src: 'https://media.comfy.org/website/fdct/headphones.png',
45+
alt: 'Product Still Workflow'
46+
},
47+
description:
48+
'Render studio-grade product stills from a single reference image.',
49+
tags: ['Image Generation']
50+
}
51+
}
52+
}
53+
54+
export const Compact: Story = {
55+
args: {
56+
variant: 'compact',
57+
item: {
58+
id: 'storyboard-to-video-seedance',
59+
title: 'Storyboard To Video - Seedance 2.0',
60+
media: {
61+
type: 'video',
62+
src: 'https://comfy-hub-assets.comfy.org/uploads/34ea9f1a-1aac-4c6f-af48-b88cf154ec9b.mp4',
63+
alt: 'Storyboard To Video - Seedance 2.0'
64+
},
65+
tags: ['Advertising', 'Film']
66+
}
67+
}
68+
}
69+
70+
export const TitleOnly: Story = {
71+
args: {
72+
item: {
73+
id: 'ltx-cleanplate-for-vfx',
74+
title: 'LTX Cleanplate for VFX',
75+
href: 'https://comfy.org/workflows/8f2cf0df5da6-8f2cf0df5da6/',
76+
media: {
77+
type: 'video',
78+
src: 'https://comfy-hub-assets.comfy.org/uploads/8a3a846f-5017-428e-b2a2-24025c55e884.mp4',
79+
alt: 'LTX Cleanplate for VFX'
80+
}
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)