fix: address PR review comments — a11y, i18n, security, DRY

- Fix SSO card showing wrong cloud copy in translations
- Add type="button" to FAQ toggles, v-show for ARIA panel linkage
- Add noreferrer to all target="_blank" links
- Use externalLinks.app instead of hardcoded URL in cloud hero
- Add contact route and use locale-aware routing for /contact
- Fix "FAQ's" → "FAQs" typo in download and cloud sections
- Remove lazy-loading from transition images to prevent pop-in
- Mark decorative side images with empty alt + aria-hidden
- Single-pass heading split in AudienceSection
- Move cardDef above usage for readability
- Export Feature interface for consumer type safety
- Extract shared badge class constant in AIModelsSection

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019d9393-67e1-75e9-9a86-9b1b1b7ef1ee
This commit is contained in:
DrJKL
2026-04-15 17:08:06 -07:00
parent 88792ffd89
commit bf1c48a441
12 changed files with 45 additions and 41 deletions

View File

@@ -59,6 +59,7 @@ function toggle(index: number) {
>
<button
:id="`faq-trigger-${index}`"
type="button"
:aria-expanded="expanded[index]"
:aria-controls="`faq-panel-${index}`"
:class="
@@ -89,8 +90,9 @@ function toggle(index: number) {
</span>
</button>
<section
v-if="expanded[index]"
v-show="expanded[index]"
:id="`faq-panel-${index}`"
role="region"
:aria-labelledby="`faq-trigger-${index}`"
class="pb-6"
>

View File

@@ -21,13 +21,6 @@ const {
const routes = getRoutes(locale)
const allCards: (ReturnType<typeof cardDef> & { product: Product })[] = [
cardDef('local', routes.download, 'bg-primary-warm-gray'),
cardDef('cloud', routes.cloud, 'bg-secondary-mauve'),
cardDef('api', routes.api, 'bg-primary-comfy-plum'),
cardDef('enterprise', routes.cloudEnterprise, 'bg-illustration-forest')
]
function cardDef(product: Product, href: string, bg: string) {
return {
product,
@@ -39,6 +32,13 @@ function cardDef(product: Product, href: string, bg: string) {
}
}
const allCards: (ReturnType<typeof cardDef> & { product: Product })[] = [
cardDef('local', routes.download, 'bg-primary-warm-gray'),
cardDef('cloud', routes.cloud, 'bg-secondary-mauve'),
cardDef('api', routes.api, 'bg-primary-comfy-plum'),
cardDef('enterprise', routes.cloudEnterprise, 'bg-illustration-forest')
]
const cards = excludeProduct
? allCards.filter((c) => c.product !== excludeProduct)
: allCards

View File

@@ -92,10 +92,9 @@ useParallax([leftImgRef], { trigger: sectionRef, y: -60 })
<img
:key="activeLeft"
:src="activeLeft"
:alt="activeLabel"
alt=""
aria-hidden="true"
class="absolute inset-0 size-full object-cover"
loading="lazy"
decoding="async"
/>
</Transition>
</div>
@@ -108,10 +107,9 @@ useParallax([leftImgRef], { trigger: sectionRef, y: -60 })
<img
:key="activeRight"
:src="activeRight"
:alt="activeLabel"
alt=""
aria-hidden="true"
class="absolute inset-0 size-full object-cover"
loading="lazy"
decoding="async"
/>
</Transition>
</div>

View File

@@ -32,7 +32,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
<a
:href="externalLinks.app"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
>
{{ t('api.hero.getApiKeys', locale) }}
@@ -40,7 +40,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
<a
:href="externalLinks.docs"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink rounded-full border px-8 py-4 text-center text-sm font-bold tracking-wider transition-colors lg:min-w-60"
>
{{ t('api.hero.viewDocs', locale) }}

View File

@@ -22,45 +22,43 @@ type ModelCard = {
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const badgeBase =
'bg-white/20 text-white backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink'
const modelCards: ModelCard[] = [
{
titleKey: 'cloud.aiModels.card.grokImagine',
imageSrc: '/images/cloud/ai-models/grok-imagine.webp',
badgeIcon: '/icons/ai-models/grok.svg',
badgeClass:
'bg-white/20 text-white rounded-2xl backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
badgeClass: `${badgeBase} rounded-2xl`,
layoutClass: 'lg:col-span-6 lg:aspect-[16/7]'
},
{
titleKey: 'cloud.aiModels.card.nanoBananaPro',
imageSrc: '/images/cloud/ai-models/nano-banana-pro.webp',
badgeIcon: '/icons/ai-models/gemini.svg',
badgeClass:
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
badgeClass: `${badgeBase} rounded-full`,
layoutClass: 'lg:col-span-6 lg:aspect-[16/7]'
},
{
titleKey: 'cloud.aiModels.card.ltx23',
imageSrc: '/images/cloud/ai-models/ltx-23.webp',
badgeIcon: '/icons/ai-models/ltx.svg',
badgeClass:
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
badgeClass: `${badgeBase} rounded-full`,
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
},
{
titleKey: 'cloud.aiModels.card.qwenImageEdit',
imageSrc: '/images/cloud/ai-models/qwen-image-edit.webp',
badgeIcon: '/icons/ai-models/qwen.svg',
badgeClass:
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
badgeClass: `${badgeBase} rounded-full`,
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
},
{
titleKey: 'cloud.aiModels.card.wan22TextToVideo',
imageSrc: '/images/cloud/ai-models/wan-22.webp',
badgeIcon: '/icons/ai-models/wan.svg',
badgeClass:
'bg-white/20 text-white rounded-full backdrop-blur-sm group-hover:bg-primary-comfy-yellow group-hover:text-primary-comfy-ink',
badgeClass: `${badgeBase} rounded-full`,
layoutClass: 'lg:col-span-4 lg:aspect-[4/3]'
}
]

View File

@@ -5,6 +5,8 @@ import { t } from '../../../i18n/translations'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const headingParts = t('cloud.audience.heading', locale).split('{creators}')
const cards = [
{
labelKey: 'cloud.audience.creators.label' as const,
@@ -26,11 +28,11 @@ const cards = [
<h2
class="text-primary-comfy-canvas text-3.5xl/tight mx-auto max-w-3xl text-center font-light lg:text-5xl/tight"
>
{{ t('cloud.audience.heading', locale).split('{creators}')[0]
{{ headingParts[0]
}}<span class="text-white">{{
t('cloud.audience.headingHighlight', locale)
}}</span
>{{ t('cloud.audience.heading', locale).split('{creators}')[1] }}
>{{ headingParts[1] }}
</h2>
<div

View File

@@ -1,6 +1,7 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { externalLinks } from '../../../config/routes'
import { t } from '../../../i18n/translations'
import BrandButton from '../../common/BrandButton.vue'
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
@@ -25,7 +26,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
</p>
<BrandButton
href="https://www.comfy.org"
:href="externalLinks.app"
:label="t('cloud.hero.cta', locale)"
class="mt-12 w-full text-center lg:mt-8 lg:w-auto"
/>

View File

@@ -1,10 +1,13 @@
<script setup lang="ts">
import type { Locale } from '../../../i18n/translations'
import { getRoutes } from '../../../config/routes'
import { t } from '../../../i18n/translations'
import ProductHeroBadge from '../../common/ProductHeroBadge.vue'
const { locale = 'en' } = defineProps<{ locale?: Locale }>()
const routes = getRoutes(locale)
</script>
<template>
@@ -29,7 +32,7 @@ const { locale = 'en' } = defineProps<{ locale?: Locale }>()
class="mt-10 flex w-full max-w-md flex-col lg:w-auto lg:max-w-none"
>
<a
href="/contact"
:href="routes.contact"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
>
{{ t('enterprise.hero.contactSales', locale) }}

View File

@@ -24,7 +24,7 @@ const features = [
title: t('enterprise.team.feature3.title', locale),
description: t('enterprise.team.feature3.description', locale),
ctaText: t('enterprise.hero.contactSales', locale),
ctaHref: '/contact'
ctaHref: routes.contact
}
]
</script>

View File

@@ -34,7 +34,7 @@ const { downloadUrl } = useDownloadUrl()
<a
:href="downloadUrl"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
class="bg-primary-comfy-yellow text-primary-comfy-ink rounded-full px-8 py-4 text-center text-sm font-bold tracking-wider transition-opacity hover:opacity-90 lg:min-w-60"
>
{{ t('download.hero.downloadLocal', locale) }}
@@ -42,7 +42,7 @@ const { downloadUrl } = useDownloadUrl()
<a
:href="externalLinks.github"
target="_blank"
rel="noopener"
rel="noopener noreferrer"
class="border-primary-comfy-yellow text-primary-comfy-yellow hover:bg-primary-comfy-yellow hover:text-primary-comfy-ink flex items-center justify-center gap-2 rounded-full border px-8 py-4 text-sm font-bold tracking-wider transition-colors lg:min-w-60"
>
<span

View File

@@ -14,7 +14,8 @@ const baseRoutes = {
termsOfService: '/terms-of-service',
privacyPolicy: '/privacy-policy',
videos: '/videos',
caseStudies: '/case-studies'
caseStudies: '/case-studies',
contact: '/contact'
} as const
type Routes = typeof baseRoutes

View File

@@ -261,13 +261,12 @@ const translations = {
'zh-CN': '单点登录'
},
'enterprise.team.feature2.description': {
en: 'Most things you build locally run on Comfy Cloud \u2014 same file, same results, powerful GPUs on demand. When a job outgrows your machine, push it to the cloud. No conversion, no rework.',
'zh-CN':
'你在本地构建的大部分内容都能在 Comfy Cloud 上运行——相同文件、相同结果、按需使用强大 GPU。当任务超出你的机器能力时推送到云端。无需转换无需返工。'
en: 'Enable secure, centralized user authentication across your organization with SSO and SCIM provisioning.',
'zh-CN': '为您的组织启用集中式安全用户认证,支持 SSO 和 SCIM 配置。'
},
'enterprise.team.feature2.cta': {
en: 'SEE CLOUD FEATURES',
'zh-CN': '查看云端特性'
en: 'SET UP SSO',
'zh-CN': '设置 SSO'
},
'enterprise.team.feature3.title': {
en: 'App Mode',
@@ -548,7 +547,7 @@ const translations = {
// Download FAQSection
'download.faq.heading': {
en: "FAQ's",
en: 'FAQs',
'zh-CN': '常见问题'
},
'download.faq.1.q': {
@@ -878,7 +877,7 @@ const translations = {
// Cloud FAQSection
'cloud.faq.heading': {
en: "FAQ's",
en: 'FAQs',
'zh-CN': '常见问题'
},
'cloud.faq.1.q': {