Compare commits

...

4 Commits

Author SHA1 Message Date
Michael B
d34d59a7b2 feat(website): add live-stream subscribe banner to /drops
Page-scoped purple announcement bar at the top of /drops and
/zh-CN/drops with a sign-up link that opens YouTube in a new tab
(temporary V1 fallback until an event-registration URL is provided).
Adds drops.banner.* translations and an e2e test asserting the text and
link behavior in both locales.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-19 11:28:46 -04:00
Michael B
3e1039c624 refactor(website): tighten drops hero block and spec
Convert HeroCenter01's resolveRel arrow expression to a function
declaration (per project convention), and extract a heroSection helper
in the drops e2e spec to remove duplicated locator scaffolding across
the two CTA tests.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-19 11:15:47 -04:00
Michael B
eb83aa5253 feat(website): add centered hero to /drops landing page
Adds HeroCenter01, a generic centered hero block, with the Comfy
wordmark, "Everything new in ComfyUI" headline, and primary/secondary
CTAs (Download Desktop + Launch Cloud). Mounted on both /drops and
/zh-CN/drops via a page-scoped HeroSection template. Built on the
shadcn-vue Button (the BrandButton replacement) and types Cta target/rel
from Vue's AnchorHTMLAttributes instead of a hardcoded union.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-19 11:06:15 -04:00
Michael B
90618c2c5a feat(website): add /drops landing page skeleton (en + zh-CN)
Scaffolds the /drops route as the foundation for the upcoming "Latest
Drops" marketing page. Adds the English page, zh-CN counterpart, route
entry, head metadata, and a Playwright smoke spec covering both locales
and indexability. Section components (hero, banner, grid, CTA) land in
follow-up slices.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-06-19 10:30:11 -04:00
8 changed files with 342 additions and 0 deletions

View File

@@ -0,0 +1,112 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'
import { externalLinks } from '../src/config/routes'
import type { Locale } from '../src/i18n/translations'
import { t } from '../src/i18n/translations'
import { test } from './fixtures/blockExternalMedia'
const PATH_EN = '/drops'
const PATH_ZH = '/zh-CN/drops'
const CLOUD_URL = 'https://cloud.comfy.org'
function heroSection(page: Page, locale: Locale) {
return page.locator('section').filter({
has: page.getByRole('heading', {
level: 1,
name: t('drops.hero.title', locale)
})
})
}
test.describe('Drops landing — desktop @smoke', () => {
test('renders the configured title at /drops', async ({ page }) => {
await page.goto(PATH_EN)
await expect(page).toHaveTitle(t('drops.page.title', 'en'))
})
test('renders the localized title at /zh-CN/drops', async ({ page }) => {
await page.goto(PATH_ZH)
await expect(page).toHaveTitle(t('drops.page.title', 'zh-CN'))
})
test('is indexable at both locales', async ({ page }) => {
await page.goto(PATH_EN)
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
await page.goto(PATH_ZH)
await expect(page.locator('meta[name="robots"]')).toHaveCount(0)
})
test('hero h1 renders the localized title in both locales', async ({
page
}) => {
await page.goto(PATH_EN)
await expect(
page.getByRole('heading', {
level: 1,
name: t('drops.hero.title', 'en')
})
).toBeVisible()
await page.goto(PATH_ZH)
await expect(
page.getByRole('heading', {
level: 1,
name: t('drops.hero.title', 'zh-CN')
})
).toBeVisible()
})
test('hero primary CTA links to /download per locale', async ({ page }) => {
for (const [path, locale, expectedHref] of [
[PATH_EN, 'en', '/download'],
[PATH_ZH, 'zh-CN', '/zh-CN/download']
] as const) {
await page.goto(path)
const primary = heroSection(page, locale).getByRole('link', {
name: t('drops.hero.primary', locale)
})
await expect(primary).toBeVisible()
await expect(primary).toHaveAttribute('href', expectedHref)
}
})
test('hero secondary CTA opens external Cloud in a new tab on both locales', async ({
page
}) => {
for (const [path, locale] of [
[PATH_EN, 'en'],
[PATH_ZH, 'zh-CN']
] as const) {
await page.goto(path)
const secondary = heroSection(page, locale).getByRole('link', {
name: t('drops.hero.secondary', locale)
})
await expect(secondary).toBeVisible()
await expect(secondary).toHaveAttribute('href', CLOUD_URL)
await expect(secondary).toHaveAttribute('target', '_blank')
await expect(secondary).toHaveAttribute('rel', 'noopener noreferrer')
}
})
test('subscribe banner shows text and a sign-up link in both locales', async ({
page
}) => {
for (const [path, locale] of [
[PATH_EN, 'en'],
[PATH_ZH, 'zh-CN']
] as const) {
await page.goto(path)
await expect(page.getByText(t('drops.banner.text', locale))).toBeVisible()
const signUp = page.getByRole('link', {
name: t('drops.banner.cta', locale)
})
await expect(signUp).toBeVisible()
await expect(signUp).toHaveAttribute('href', externalLinks.youtube)
await expect(signUp).toHaveAttribute('target', '_blank')
await expect(signUp).toHaveAttribute('rel', 'noopener noreferrer')
}
})
})

View File

@@ -0,0 +1,95 @@
<script setup lang="ts">
import type { AnchorHTMLAttributes } from 'vue'
import Button from '../ui/button/Button.vue'
type Cta = {
label: string
href: string
target?: AnchorHTMLAttributes['target']
rel?: AnchorHTMLAttributes['rel']
}
type Visual = {
src: string
alt: string
width?: number
height?: number
}
const { visual, eyebrow, title, subtitle, primaryCta, secondaryCta } =
defineProps<{
visual?: Visual
eyebrow?: string
title: string
subtitle?: string
primaryCta: Cta
secondaryCta?: Cta
}>()
function resolveRel(cta: Cta): AnchorHTMLAttributes['rel'] {
return (
cta.rel ?? (cta.target === '_blank' ? 'noopener noreferrer' : undefined)
)
}
</script>
<template>
<section
class="max-w-9xl mx-auto flex flex-col items-center px-6 py-16 text-center lg:py-24"
>
<img
v-if="visual"
:src="visual.src"
:alt="visual.alt"
:width="visual.width"
:height="visual.height"
fetchpriority="high"
decoding="async"
class="mb-10 h-auto w-full max-w-md lg:mb-12 lg:max-w-lg"
/>
<p
v-if="eyebrow"
class="mb-4 text-sm font-medium tracking-wide text-primary-comfy-canvas/70 uppercase"
>
{{ eyebrow }}
</p>
<h1
class="text-4xl font-light tracking-tight text-primary-comfy-canvas lg:text-6xl"
>
{{ title }}
</h1>
<p
v-if="subtitle"
class="mt-6 max-w-2xl text-base text-primary-comfy-canvas/70 lg:text-lg"
>
{{ subtitle }}
</p>
<div class="mt-10 flex flex-col gap-4 sm:flex-row lg:mt-12">
<Button
as="a"
:href="primaryCta.href"
:target="primaryCta.target"
:rel="resolveRel(primaryCta)"
size="lg"
>
{{ primaryCta.label }}
</Button>
<Button
v-if="secondaryCta"
as="a"
:href="secondaryCta.href"
:target="secondaryCta.target"
:rel="resolveRel(secondaryCta)"
variant="outline"
size="lg"
>
{{ secondaryCta.label }}
</Button>
</div>
</section>
</template>

View File

@@ -8,6 +8,7 @@ const baseRoutes = {
cloudEnterprise: '/cloud/enterprise',
api: '/api',
gallery: '/gallery',
drops: '/drops',
about: '/about',
careers: '/careers',
customers: '/customers',

View File

@@ -4928,6 +4928,48 @@ const translations = {
'affiliate.cta.termsLabel': {
en: 'Read the affiliate program terms',
'zh-CN': '阅读联盟计划条款'
},
// Drops page (/drops) — head metadata
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'drops.page.title': {
en: 'Drops — Everything new in ComfyUI',
'zh-CN': 'Drops — ComfyUI 最新动态'
},
'drops.page.description': {
en: 'Explore everything new in Comfy — releases, features, models, and resources across platform, cloud, community, and developer tools.',
'zh-CN':
'探索 Comfy 的最新动态 — 涵盖平台、云端、社区和开发者工具的发布、功能、模型和资源。'
},
// Drops page (/drops) — hero section
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'drops.hero.title': {
en: 'Everything new in ComfyUI',
'zh-CN': 'ComfyUI 全新内容'
},
'drops.hero.primary': {
en: 'Download Desktop',
'zh-CN': '下载桌面版'
},
'drops.hero.secondary': {
en: 'Launch Cloud',
'zh-CN': '启动云端'
},
'drops.hero.visualAlt': {
en: 'Comfy',
'zh-CN': 'Comfy'
},
// Drops page (/drops) — subscribe banner
// zh-CN strings pending native review (see apps/website/.scratch/drops-page/PRD.md)
'drops.banner.text': {
en: 'Subscribe to the live stream and get your questions answered in real time.',
'zh-CN': '订阅直播,实时获得您的问题解答。'
},
'drops.banner.cta': {
en: 'Sign up now',
'zh-CN': '立即注册'
}
} as const satisfies Record<string, Record<Locale, string>>

View File

@@ -0,0 +1,16 @@
---
import BaseLayout from '../layouts/BaseLayout.astro'
import HeroSection from '../templates/drops/HeroSection.vue'
import SubscribeBanner from '../templates/drops/SubscribeBanner.vue'
import { t } from '../i18n/translations'
const locale = 'en' as const
---
<BaseLayout
title={t('drops.page.title', locale)}
description={t('drops.page.description', locale)}
>
<SubscribeBanner locale={locale} />
<HeroSection locale={locale} />
</BaseLayout>

View File

@@ -0,0 +1,16 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import HeroSection from '../../templates/drops/HeroSection.vue'
import SubscribeBanner from '../../templates/drops/SubscribeBanner.vue'
import { t } from '../../i18n/translations'
const locale = 'zh-CN' as const
---
<BaseLayout
title={t('drops.page.title', locale)}
description={t('drops.page.description', locale)}
>
<SubscribeBanner locale={locale} />
<HeroSection locale={locale} />
</BaseLayout>

View File

@@ -0,0 +1,32 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import HeroCenter01 from '../../components/blocks/HeroCenter01.vue'
import { externalLinks, getRoutes } from '../../config/routes'
import { t } from '../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
</script>
<template>
<HeroCenter01
:visual="{
src: '/affiliates/brand/comfy-amplified-logo.png',
alt: t('drops.hero.visualAlt', locale),
width: 632,
height: 632
}"
:title="t('drops.hero.title', locale)"
:primary-cta="{
label: t('drops.hero.primary', locale),
href: routes.download
}"
:secondary-cta="{
label: t('drops.hero.secondary', locale),
href: externalLinks.cloud,
target: '_blank'
}"
/>
</template>

View File

@@ -0,0 +1,28 @@
<script setup lang="ts">
import type { Locale } from '../../i18n/translations'
import { externalLinks } from '../../config/routes'
import { t } from '../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
// V1 fallback: point at the Comfy YouTube channel until content team supplies
// a dedicated event-registration URL (Google Form, Eventbrite, etc.).
const signUpHref = externalLinks.youtube
</script>
<template>
<div
class="bg-primary-comfy-plum text-primary-warm-white flex w-full flex-col items-center justify-center gap-2 px-6 py-3 text-center text-sm sm:flex-row sm:gap-4"
>
<p>{{ t('drops.banner.text', locale) }}</p>
<a
:href="signUpHref"
target="_blank"
rel="noopener noreferrer"
class="font-semibold uppercase underline underline-offset-4 hover:no-underline"
>
{{ t('drops.banner.cta', locale) }}
</a>
</div>
</template>