Skip to content
Merged
28 changes: 25 additions & 3 deletions apps/website/astro.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,23 @@ import sitemap from '@astrojs/sitemap'
import vue from '@astrojs/vue'
import tailwindcss from '@tailwindcss/vite'

const LOCALES = ['en', 'zh-CN'] as const
const DEFAULT_LOCALE = 'en'
const PAYMENT_STATUSES = ['success', 'failed'] as const
const LOCALE_PREFIXES = LOCALES.map((locale) =>
locale === DEFAULT_LOCALE ? '' : `/${locale}`
)
const SITEMAP_EXCLUDED_PATHNAMES = new Set(
LOCALE_PREFIXES.flatMap((prefix) =>
PAYMENT_STATUSES.map((status) => `${prefix}/payment/${status}`)
)
)

function isExcludedFromSitemap(page: string): boolean {
const pathname = new URL(page).pathname.replace(/\/$/, '')
return SITEMAP_EXCLUDED_PATHNAMES.has(pathname)
}

export default defineConfig({
site: 'https://comfy.org',
output: 'static',
Expand All @@ -17,7 +34,12 @@ export default defineConfig({
assets: '_website'
},
devToolbar: { enabled: !process.env.NO_TOOLBAR },
integrations: [vue(), sitemap()],
integrations: [
vue(),
sitemap({
filter: (page) => !isExcludedFromSitemap(page)
})
],
vite: {
plugins: [tailwindcss()],
server: {
Expand All @@ -27,8 +49,8 @@ export default defineConfig({
}
},
i18n: {
locales: ['en', 'zh-CN'],
defaultLocale: 'en',
locales: [...LOCALES],
defaultLocale: DEFAULT_LOCALE,
routing: {
prefixDefaultLocale: false
}
Expand Down
115 changes: 115 additions & 0 deletions apps/website/e2e/payment.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { Page } from '@playwright/test'
import { expect } from '@playwright/test'

import { externalLinks } from '../src/config/routes'
import { test } from './fixtures/blockExternalMedia'

const CLOUD_URL = externalLinks.cloud
const PLATFORM_USAGE_URL = externalLinks.platformUsage
const SUPPORT_URL = externalLinks.support
const DOCS_SUBSCRIPTION_URL = externalLinks.docsSubscription

async function expectNoIndex(page: Page) {
await expect(page.locator('meta[name="robots"]')).toHaveAttribute(
'content',
'noindex, nofollow'
)
}

test.describe('Payment success page @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/payment/success')
})

test('has correct title and is noindex', async ({ page }) => {
await expect(page).toHaveTitle('Payment Successful — Comfy')
await expectNoIndex(page)
})

test('shows success heading and subtitle', async ({ page }) => {
await expect(
page.getByRole('heading', { name: /Payment successful/i, level: 1 })
).toBeVisible()
await expect(page.getByText(/Thanks for your purchase/i)).toBeVisible()
})

test('primary CTA links to Comfy Cloud', async ({ page }) => {
const cta = page.getByRole('link', { name: /CONTINUE TO COMFY CLOUD/i })
await expect(cta).toBeVisible()
await expect(cta).toHaveAttribute('href', CLOUD_URL)
})

test('secondary CTA links to platform usage & payments page', async ({
page
}) => {
const cta = page.getByRole('link', { name: /VIEW USAGE & PAYMENTS/i })
await expect(cta).toBeVisible()
await expect(cta).toHaveAttribute('href', PLATFORM_USAGE_URL)
})
})

test.describe('Payment failed page @smoke', () => {
test.beforeEach(async ({ page }) => {
await page.goto('/payment/failed')
})

test('has correct title and is noindex', async ({ page }) => {
await expect(page).toHaveTitle('Payment Failed — Comfy')
await expectNoIndex(page)
})

test('shows failure heading and subtitle', async ({ page }) => {
await expect(
page.getByRole('heading', {
name: /Payment was not completed/i,
level: 1
})
).toBeVisible()
await expect(page.getByText(/payment didn't go through/i)).toBeVisible()
})

test('primary CTA links to support help center', async ({ page }) => {
const cta = page.getByRole('link', { name: /CONTACT SUPPORT/i })
await expect(cta).toBeVisible()
await expect(cta).toHaveAttribute('href', SUPPORT_URL)
})

test('secondary CTA links to subscription docs', async ({ page }) => {
const cta = page.getByRole('link', { name: /READ SUBSCRIPTION DOCS/i })
await expect(cta).toBeVisible()
await expect(cta).toHaveAttribute('href', DOCS_SUBSCRIPTION_URL)
})
})

test.describe('Payment pages zh-CN @smoke', () => {
test('zh-CN success page renders and links correctly', async ({ page }) => {
await page.goto('/zh-CN/payment/success')
await expect(page).toHaveTitle('支付成功 — Comfy')
await expectNoIndex(page)
await expect(
page.getByRole('heading', { name: '支付成功', level: 1 })
).toBeVisible()
await expect(
page.getByRole('link', { name: '前往 COMFY CLOUD' })
).toHaveAttribute('href', CLOUD_URL)
await expect(
page.getByRole('link', { name: '查看用量与支付' })
).toHaveAttribute('href', PLATFORM_USAGE_URL)
})

test('zh-CN failed page renders and links correctly', async ({ page }) => {
await page.goto('/zh-CN/payment/failed')
await expect(page).toHaveTitle('支付失败 — Comfy')
await expectNoIndex(page)
await expect(
page.getByRole('heading', { name: '支付未完成', level: 1 })
).toBeVisible()
await expect(page.getByRole('link', { name: '联系支持' })).toHaveAttribute(
'href',
SUPPORT_URL
)
await expect(
page.getByRole('link', { name: '查看订阅文档' })
).toHaveAttribute('href', DOCS_SUBSCRIPTION_URL)
})
})
1 change: 1 addition & 0 deletions apps/website/public/robots.txt
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,6 @@ Allow: /
Disallow: /_astro/
Disallow: /_website/
Disallow: /_vercel/
Disallow: /payment/

Sitemap: https://comfy.org/sitemap-index.xml
101 changes: 101 additions & 0 deletions apps/website/src/components/payment/PaymentStatusSection.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
<script setup lang="ts">
import { cn } from '@comfyorg/tailwind-utils'

import { externalLinks } from '../../config/routes'
import type { Locale } from '../../i18n/translations'
import { t } from '../../i18n/translations'
import BrandButton from '../common/BrandButton.vue'
import SectionLabel from '../common/SectionLabel.vue'

// Display-only thank-you / failure pages: payment state is verified
// server-side via Stripe webhooks (see comfy-api). These pages exist
// solely as the redirect target for Stripe Checkout.

type Status = 'success' | 'failed'

const { status, locale = 'en' } = defineProps<{
status: Status
locale?: Locale
}>()

const primaryHref =
status === 'success' ? externalLinks.cloud : externalLinks.support
const secondaryHref =
status === 'success'
? externalLinks.platformUsage
: externalLinks.docsSubscription

const iconRingClass =
status === 'success'
? 'border-primary-comfy-yellow text-primary-comfy-yellow'
: 'border-secondary-mauve text-secondary-mauve'
</script>

<template>
<section
class="flex min-h-[calc(100dvh-12rem)] items-center justify-center px-6 py-16 lg:py-24"
>
<div class="flex max-w-2xl flex-col items-center gap-6 text-center">
<div
:class="
cn(
'flex size-20 items-center justify-center rounded-full border-2',
iconRingClass
)
"
aria-hidden="true"
>
<svg
v-if="status === 'success'"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="size-10"
>
<path d="M5 12.5l4.5 4.5L19 7.5" />
</svg>
<svg
v-else
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
class="size-10"
>
<path d="M6 6l12 12" />
<path d="M18 6L6 18" />
</svg>
</div>

<SectionLabel>{{ t(`payment.${status}.label`, locale) }}</SectionLabel>

<h1
class="text-primary-comfy-canvas text-4xl/tight font-light md:text-5xl/tight lg:text-6xl/tight"
>
{{ t(`payment.${status}.title`, locale) }}
</h1>

<p
class="text-primary-comfy-canvas/80 max-w-xl text-base font-light lg:text-lg"
>
{{ t(`payment.${status}.subtitle`, locale) }}
</p>

<div
class="mt-2 flex flex-col items-stretch gap-3 sm:flex-row sm:items-center sm:justify-center"
>
<BrandButton :href="primaryHref" variant="solid" size="nav">
{{ t(`payment.${status}.primaryCta`, locale) }}
</BrandButton>
<BrandButton :href="secondaryHref" variant="outline" size="nav">
{{ t(`payment.${status}.secondaryCta`, locale) }}
</BrandButton>
</div>
</div>
</section>
</template>
2 changes: 2 additions & 0 deletions apps/website/src/config/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ export const externalLinks = {
discord: 'https://discord.com/invite/comfyorg',
docs: 'https://docs.comfy.org/',
docsApi: 'https://docs.comfy.org/api-reference/cloud',
docsSubscription: 'https://docs.comfy.org/support/subscription/subscribing',
github: 'https://github.com/Comfy-Org/ComfyUI',
platform: 'https://platform.comfy.org',
platformUsage: 'https://platform.comfy.org/profile/usage',
support: 'https://support.comfy.org/hc/en-us',
workflows: 'https://comfy.org/workflows',
youtube: 'https://www.youtube.com/@ComfyOrg'
Expand Down
43 changes: 43 additions & 0 deletions apps/website/src/i18n/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3592,6 +3592,49 @@ const translations = {
'customers.feedback.role3': {
en: 'Head of AI at Creative Studios',
'zh-CN': 'Creative Studios AI 负责人'
},

// Payment status pages
'payment.success.label': {
en: 'PAYMENT',
'zh-CN': '支付'
},
'payment.success.title': {
en: 'Payment successful',
'zh-CN': '支付成功'
},
'payment.success.subtitle': {
en: "Thanks for your purchase. Your account has been credited and you're ready to keep building.",
'zh-CN': '感谢您的购买。您的账户已充值完成,可以继续创作了。'
},
'payment.success.primaryCta': {
en: 'CONTINUE TO COMFY CLOUD',
'zh-CN': '前往 COMFY CLOUD'
},
'payment.success.secondaryCta': {
en: 'VIEW USAGE & PAYMENTS',
'zh-CN': '查看用量与支付'
},
'payment.failed.label': {
en: 'PAYMENT',
'zh-CN': '支付'
},
'payment.failed.title': {
en: 'Payment was not completed',
'zh-CN': '支付未完成'
},
'payment.failed.subtitle': {
en: "Your payment didn't go through and you have not been charged. Reach out to support or read the subscription docs if you need help.",
'zh-CN':
'您的支付未能完成,未发生扣款。如需帮助,请联系支持或查阅订阅文档。'
},
'payment.failed.primaryCta': {
en: 'CONTACT SUPPORT',
'zh-CN': '联系支持'
},
'payment.failed.secondaryCta': {
en: 'READ SUBSCRIPTION DOCS',
'zh-CN': '查看订阅文档'
}
} as const satisfies Record<string, Record<Locale, string>>

Expand Down
12 changes: 12 additions & 0 deletions apps/website/src/pages/payment/failed.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import PaymentStatusSection from '../../components/payment/PaymentStatusSection.vue'
---

<BaseLayout
title="Payment Failed — Comfy"
description="Your payment was not completed."
noindex
>
<PaymentStatusSection status="failed" />
</BaseLayout>
12 changes: 12 additions & 0 deletions apps/website/src/pages/payment/success.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro'
import PaymentStatusSection from '../../components/payment/PaymentStatusSection.vue'
---

<BaseLayout
title="Payment Successful — Comfy"
description="Your payment was processed successfully."
noindex
>
<PaymentStatusSection status="success" />
</BaseLayout>
8 changes: 8 additions & 0 deletions apps/website/src/pages/zh-CN/payment/failed.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro'
import PaymentStatusSection from '../../../components/payment/PaymentStatusSection.vue'
---

<BaseLayout title="支付失败 — Comfy" description="您的支付未能完成。" noindex>
<PaymentStatusSection status="failed" locale="zh-CN" />
</BaseLayout>
8 changes: 8 additions & 0 deletions apps/website/src/pages/zh-CN/payment/success.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
import BaseLayout from '../../../layouts/BaseLayout.astro'
import PaymentStatusSection from '../../../components/payment/PaymentStatusSection.vue'
---

<BaseLayout title="支付成功 — Comfy" description="您的支付已成功完成。" noindex>
<PaymentStatusSection status="success" locale="zh-CN" />
</BaseLayout>
Loading