From 7bd99ee98c5b736f7e9ee43ffd2ab301a736ca05 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Wed, 6 May 2026 01:19:53 +0000 Subject: [PATCH] feat(website): draft Affiliate Program landing page at /affiliates Conversion-oriented English landing page that drives applications to the Comfy Affiliate Program Google Form. Page is shipped behind noindex while copy and assets are still being finalized; a follow-up PR will flip it indexable and add the zh-CN locale. Sections (in order): hero, trust band, how-it-works, who-we're-looking-for, program details, brand assets, FAQ, footer CTA. Mirrors the type scale, brand tokens, and noindex/sitemap/robots conventions used by the /affiliates/terms PR (#11954) so the two pages feel like a set. - Copy lives under `affiliate-landing.*` in src/i18n/translations.ts (en only; zh-CN values mirror en until a localized version lands). - Reuses common/FAQSection, common/BrandButton, common/SectionHeader, common/SectionLabel verbatim. Sections without a clean primitive fit (split hero, trust band, 3-step flow, audience list, details table, brand-asset grid, footer CTA) render inline with brand tokens rather than forking new shared components. - Brand-assets grid wired via a config object in components/affiliates/brandAssets.ts so 728x90 / 300x250 / 160x600 / 1200x628 banners can be dropped in without code changes. - FAQ emits FAQPage JSON-LD (schema.org) so the page is structured-data ready when noindex flips off. - Adds /affiliates to NOINDEX_PATHNAMES (excluded from sitemap), to robots.txt Disallow under every UA block (including AI-bot overrides), and renders ``. - Adds `rel` prop to common/BrandButton.vue so external CTAs can pass rel="noopener noreferrer" alongside target="_blank". - Vitest unit test asserts i18n key structure (no internal-only keys, ordered sections, every indexed item has a translation). - Playwright e2e covers desktop+mobile render/visibility under @smoke, CTA click and FAQ toggle interactions, FAQPage JSON-LD presence, responsive table-to-definition-list collapse. --- apps/website/astro.config.ts | 8 +- apps/website/e2e/affiliates.spec.ts | 169 ++++++++++ apps/website/public/robots.txt | 9 + .../components/affiliates/AudienceSection.vue | 42 +++ .../affiliates/BrandAssetsSection.vue | 83 +++++ .../affiliates/FooterCtaSection.vue | 43 +++ .../src/components/affiliates/HeroSection.vue | 97 ++++++ .../affiliates/HowItWorksSection.vue | 55 ++++ .../affiliates/ProgramDetailsSection.vue | 105 ++++++ .../affiliates/TrustBandSection.vue | 20 ++ .../components/affiliates/affiliateFaqs.ts | 6 + .../affiliates/affiliateLanding.test.ts | 144 +++++++++ .../src/components/affiliates/brandAssets.ts | 44 +++ .../src/components/common/BrandButton.vue | 3 + apps/website/src/config/routes.ts | 3 + apps/website/src/i18n/translations.ts | 305 ++++++++++++++++++ apps/website/src/pages/affiliates/index.astro | 66 ++++ 17 files changed, 1199 insertions(+), 3 deletions(-) create mode 100644 apps/website/e2e/affiliates.spec.ts create mode 100644 apps/website/src/components/affiliates/AudienceSection.vue create mode 100644 apps/website/src/components/affiliates/BrandAssetsSection.vue create mode 100644 apps/website/src/components/affiliates/FooterCtaSection.vue create mode 100644 apps/website/src/components/affiliates/HeroSection.vue create mode 100644 apps/website/src/components/affiliates/HowItWorksSection.vue create mode 100644 apps/website/src/components/affiliates/ProgramDetailsSection.vue create mode 100644 apps/website/src/components/affiliates/TrustBandSection.vue create mode 100644 apps/website/src/components/affiliates/affiliateFaqs.ts create mode 100644 apps/website/src/components/affiliates/affiliateLanding.test.ts create mode 100644 apps/website/src/components/affiliates/brandAssets.ts create mode 100644 apps/website/src/pages/affiliates/index.astro diff --git a/apps/website/astro.config.ts b/apps/website/astro.config.ts index 6c542b3a6b..f67284034a 100644 --- a/apps/website/astro.config.ts +++ b/apps/website/astro.config.ts @@ -9,10 +9,12 @@ const PAYMENT_STATUSES = ['success', 'failed'] as const const LOCALE_PREFIXES = LOCALES.map((locale) => locale === DEFAULT_LOCALE ? '' : `/${locale}` ) +const NOINDEX_PATHNAMES = ['/affiliates'] const SITEMAP_EXCLUDED_PATHNAMES = new Set( - LOCALE_PREFIXES.flatMap((prefix) => - PAYMENT_STATUSES.map((status) => `${prefix}/payment/${status}`) - ) + LOCALE_PREFIXES.flatMap((prefix) => [ + ...PAYMENT_STATUSES.map((status) => `${prefix}/payment/${status}`), + ...NOINDEX_PATHNAMES.map((path) => `${prefix}${path}`) + ]) ) function isExcludedFromSitemap(page: string): boolean { diff --git a/apps/website/e2e/affiliates.spec.ts b/apps/website/e2e/affiliates.spec.ts new file mode 100644 index 0000000000..85ac714c67 --- /dev/null +++ b/apps/website/e2e/affiliates.spec.ts @@ -0,0 +1,169 @@ +import { expect } from '@playwright/test' + +import { test } from './fixtures/blockExternalMedia' + +const PATH = '/affiliates' +const APPLY_URL = 'https://forms.gle/RS8L2ttcuGap4Q1v6' + +const SECTION_TESTIDS = [ + 'affiliate-trust-band', + 'affiliate-how-it-works', + 'affiliate-audience', + 'affiliate-program-details', + 'affiliate-brand-assets', + 'affiliate-footer-cta' +] as const + +test.describe('Affiliates landing — desktop @smoke', () => { + test.beforeEach(async ({ page }) => { + await page.goto(PATH) + }) + + test('renders the hero heading and is marked noindex', async ({ page }) => { + await expect( + page.getByRole('heading', { name: 'Become a Comfy Partner', level: 1 }) + ).toBeVisible() + + const robotsContent = await page + .locator('meta[name="robots"]') + .getAttribute('content') + expect(robotsContent).toContain('noindex') + }) + + test('exposes every page section in order', async ({ page }) => { + for (const id of SECTION_TESTIDS) { + await expect(page.getByTestId(id)).toBeVisible() + } + }) + + test('renders the program details table on desktop', async ({ page }) => { + const table = page.getByTestId('affiliate-program-details-table') + await expect(table).toBeVisible() + const rows = table.getByRole('row') + await expect(rows).toHaveCount(7) + }) + + test('emits FAQPage structured data', async ({ page }) => { + const faqJsonLd = await page.evaluate(() => { + const scripts = Array.from( + document.querySelectorAll( + 'script[type="application/ld+json"]' + ) + ) + const match = scripts.find((s) => + (s.textContent ?? '').includes('FAQPage') + ) + return match?.textContent ?? null + }) + expect(faqJsonLd, 'FAQ JSON-LD script').not.toBeNull() + const parsed = JSON.parse(faqJsonLd!) + expect(parsed['@type']).toBe('FAQPage') + expect(Array.isArray(parsed.mainEntity)).toBe(true) + expect(parsed.mainEntity.length).toBe(8) + }) + + test('hero and footer CTAs target the application form in a new tab', async ({ + page + }) => { + const heroCta = page.getByTestId('affiliate-hero-cta') + await expect(heroCta).toBeVisible() + await expect(heroCta).toHaveAttribute('href', APPLY_URL) + await expect(heroCta).toHaveAttribute('target', '_blank') + await expect(heroCta).toHaveAttribute('rel', 'noopener noreferrer') + + const footerCta = page.getByTestId('affiliate-footer-cta-button') + await expect(footerCta).toHaveAttribute('href', APPLY_URL) + await expect(footerCta).toHaveAttribute('target', '_blank') + await expect(footerCta).toHaveAttribute('rel', 'noopener noreferrer') + }) + + test('footer links to the affiliate terms page', async ({ page }) => { + const link = page + .getByTestId('affiliate-footer-cta') + .getByRole('link', { name: /Read the affiliate program terms/i }) + await expect(link).toBeVisible() + await expect(link).toBeEnabled() + await expect(link).toHaveAttribute('href', '/affiliates/terms') + await expect(link).not.toHaveAttribute('target', '_blank') + }) +}) + +test.describe('Affiliates landing — desktop interactions', () => { + test.beforeEach(async ({ page }) => { + await page.goto(PATH) + }) + + test('Apply Now CTA opens the application form in a new tab', async ({ + page, + context + }) => { + const popupPromise = context.waitForEvent('page') + await page.getByTestId('affiliate-hero-cta').click() + const popup = await popupPromise + const popupUrl = popup.url() + expect( + popupUrl.includes('forms.gle/RS8L2ttcuGap4Q1v6') || + popupUrl.includes('docs.google.com/forms') + ).toBe(true) + await popup.close() + }) + + test('FAQ items toggle open and closed on click', async ({ page }) => { + const firstQuestion = page.getByRole('button', { + name: 'How do I track my referrals?' + }) + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') + + await firstQuestion.click() + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'true') + await expect( + page.getByText('Real-time dashboard via our partner portal.') + ).toBeVisible() + + await firstQuestion.click() + await expect(firstQuestion).toHaveAttribute('aria-expanded', 'false') + }) +}) + +test.describe('Affiliates landing — mobile @mobile', () => { + test.beforeEach(async ({ page }) => { + await page.goto(PATH) + }) + + test('renders the hero heading and primary CTA at narrow viewports', async ({ + page + }) => { + await expect( + page.getByRole('heading', { name: 'Become a Comfy Partner', level: 1 }) + ).toBeVisible() + await expect(page.getByTestId('affiliate-hero-cta')).toBeVisible() + }) + + test('program details collapse to a stacked definition list', async ({ + page + }) => { + await expect( + page.getByTestId('affiliate-program-details-table') + ).toBeHidden() + const detailsList = page + .getByTestId('affiliate-program-details') + .locator('dl') + await expect(detailsList).toBeVisible() + await expect(detailsList.getByText('Commission rate')).toBeVisible() + await expect(detailsList.getByText('30% recurring')).toBeVisible() + }) + + test('all major sections remain visible without horizontal overflow', async ({ + page + }) => { + for (const id of SECTION_TESTIDS) { + const section = page.getByTestId(id) + await expect(section).toBeVisible() + const box = await section.boundingBox() + expect(box, `${id} bounding box`).not.toBeNull() + expect(box!.x + box!.width).toBeLessThanOrEqual( + page.viewportSize()!.width + 1 + ) + } + }) +}) diff --git a/apps/website/public/robots.txt b/apps/website/public/robots.txt index da06a725ac..c2b0a6dc95 100644 --- a/apps/website/public/robots.txt +++ b/apps/website/public/robots.txt @@ -30,29 +30,38 @@ Disallow: /_astro/ Disallow: /_website/ Disallow: /_vercel/ Disallow: /payment/ +Disallow: /affiliates User-agent: GPTBot Allow: / +Disallow: /affiliates User-agent: OAI-SearchBot Allow: / +Disallow: /affiliates User-agent: ChatGPT-User Allow: / +Disallow: /affiliates User-agent: ClaudeBot Allow: / +Disallow: /affiliates User-agent: Claude-User Allow: / +Disallow: /affiliates User-agent: Claude-SearchBot Allow: / +Disallow: /affiliates User-agent: PerplexityBot Allow: / +Disallow: /affiliates User-agent: Google-Extended Allow: / +Disallow: /affiliates Sitemap: https://comfy.org/sitemap-index.xml diff --git a/apps/website/src/components/affiliates/AudienceSection.vue b/apps/website/src/components/affiliates/AudienceSection.vue new file mode 100644 index 0000000000..c269276038 --- /dev/null +++ b/apps/website/src/components/affiliates/AudienceSection.vue @@ -0,0 +1,42 @@ + + + diff --git a/apps/website/src/components/affiliates/BrandAssetsSection.vue b/apps/website/src/components/affiliates/BrandAssetsSection.vue new file mode 100644 index 0000000000..08ebb351b1 --- /dev/null +++ b/apps/website/src/components/affiliates/BrandAssetsSection.vue @@ -0,0 +1,83 @@ + + + diff --git a/apps/website/src/components/affiliates/FooterCtaSection.vue b/apps/website/src/components/affiliates/FooterCtaSection.vue new file mode 100644 index 0000000000..2a961d7493 --- /dev/null +++ b/apps/website/src/components/affiliates/FooterCtaSection.vue @@ -0,0 +1,43 @@ + + + diff --git a/apps/website/src/components/affiliates/HeroSection.vue b/apps/website/src/components/affiliates/HeroSection.vue new file mode 100644 index 0000000000..92ed13ea87 --- /dev/null +++ b/apps/website/src/components/affiliates/HeroSection.vue @@ -0,0 +1,97 @@ + + + diff --git a/apps/website/src/components/affiliates/HowItWorksSection.vue b/apps/website/src/components/affiliates/HowItWorksSection.vue new file mode 100644 index 0000000000..e459c40844 --- /dev/null +++ b/apps/website/src/components/affiliates/HowItWorksSection.vue @@ -0,0 +1,55 @@ + + + diff --git a/apps/website/src/components/affiliates/ProgramDetailsSection.vue b/apps/website/src/components/affiliates/ProgramDetailsSection.vue new file mode 100644 index 0000000000..5f776f7522 --- /dev/null +++ b/apps/website/src/components/affiliates/ProgramDetailsSection.vue @@ -0,0 +1,105 @@ + + + diff --git a/apps/website/src/components/affiliates/TrustBandSection.vue b/apps/website/src/components/affiliates/TrustBandSection.vue new file mode 100644 index 0000000000..1b72122d8b --- /dev/null +++ b/apps/website/src/components/affiliates/TrustBandSection.vue @@ -0,0 +1,20 @@ + + + diff --git a/apps/website/src/components/affiliates/affiliateFaqs.ts b/apps/website/src/components/affiliates/affiliateFaqs.ts new file mode 100644 index 0000000000..9082927d0a --- /dev/null +++ b/apps/website/src/components/affiliates/affiliateFaqs.ts @@ -0,0 +1,6 @@ +import type { TranslationKey } from '../../i18n/translations' + +export const AFFILIATE_FAQ_PREFIX = 'affiliate-landing.faq' +export const AFFILIATE_FAQ_HEADING_KEY: TranslationKey = + 'affiliate-landing.faq.heading' +export const AFFILIATE_FAQ_COUNT = 8 diff --git a/apps/website/src/components/affiliates/affiliateLanding.test.ts b/apps/website/src/components/affiliates/affiliateLanding.test.ts new file mode 100644 index 0000000000..0b9cf90039 --- /dev/null +++ b/apps/website/src/components/affiliates/affiliateLanding.test.ts @@ -0,0 +1,144 @@ +import { describe, expect, it } from 'vitest' + +import { hasKey, t, translationKeys } from '../../i18n/translations' +import { + AFFILIATE_FAQ_COUNT, + AFFILIATE_FAQ_HEADING_KEY, + AFFILIATE_FAQ_PREFIX +} from './affiliateFaqs' +import { brandAssets } from './brandAssets' + +const PREFIX = 'affiliate-landing' + +const EXPECTED_SECTION_PREFIXES = [ + 'page', + 'cta', + 'hero', + 'trust', + 'how', + 'audience', + 'details', + 'assets', + 'faq', + 'footerCta' +] as const + +const HERO_HIGHLIGHT_COUNT = 4 +const HOW_STEP_COUNT = 3 +const AUDIENCE_ITEM_COUNT = 5 +const DETAILS_ROW_COUNT = 6 + +const INTERNAL_KEY_PATTERNS = [ + /open-questions/, + /todo/i, + /draft/i, + /placeholder/i, + /internal/i +] + +function affiliateKeys(): string[] { + return translationKeys.filter((k) => k.startsWith(`${PREFIX}.`)) +} + +describe('affiliate landing i18n', () => { + it('exposes the canonical top-level section prefixes', () => { + const keys = affiliateKeys() + for (const section of EXPECTED_SECTION_PREFIXES) { + const hit = keys.some((k) => k.startsWith(`${PREFIX}.${section}.`)) + expect(hit, `missing section: ${section}`).toBe(true) + } + }) + + it('orders sections as the page renders them', () => { + const keys = affiliateKeys() + const seenSections: string[] = [] + for (const key of keys) { + const section = key.split('.')[1] + if (!section) continue + if (!seenSections.includes(section)) seenSections.push(section) + } + const orderedExpected = EXPECTED_SECTION_PREFIXES.filter((s) => + seenSections.includes(s) + ) + const orderedActual = seenSections.filter((s) => + (EXPECTED_SECTION_PREFIXES as readonly string[]).includes(s) + ) + expect(orderedActual).toEqual([...orderedExpected]) + }) + + it('exposes hero, page, and cta keys editors will need', () => { + expect(hasKey(`${PREFIX}.page.title`)).toBe(true) + expect(hasKey(`${PREFIX}.page.description`)).toBe(true) + expect(hasKey(`${PREFIX}.cta.apply`)).toBe(true) + expect(hasKey(`${PREFIX}.cta.applyAriaLabel`)).toBe(true) + expect(hasKey(`${PREFIX}.hero.heading`)).toBe(true) + expect(hasKey(`${PREFIX}.hero.subheading`)).toBe(true) + expect(hasKey(`${PREFIX}.hero.body`)).toBe(true) + for (let i = 0; i < HERO_HIGHLIGHT_COUNT; i++) { + expect(hasKey(`${PREFIX}.hero.highlight.${i}`)).toBe(true) + } + }) + + it('exposes the trust band, how-it-works, and audience copy', () => { + expect(hasKey(`${PREFIX}.trust.label`)).toBe(true) + expect(hasKey(`${PREFIX}.how.heading`)).toBe(true) + for (let i = 0; i < HOW_STEP_COUNT; i++) { + expect(hasKey(`${PREFIX}.how.step.${i}.title`)).toBe(true) + expect(hasKey(`${PREFIX}.how.step.${i}.body`)).toBe(true) + } + expect(hasKey(`${PREFIX}.audience.heading`)).toBe(true) + for (let i = 0; i < AUDIENCE_ITEM_COUNT; i++) { + expect(hasKey(`${PREFIX}.audience.item.${i}`)).toBe(true) + } + }) + + it('exposes the program details rows', () => { + expect(hasKey(`${PREFIX}.details.heading`)).toBe(true) + expect(hasKey(`${PREFIX}.details.headerLabel`)).toBe(true) + expect(hasKey(`${PREFIX}.details.headerValue`)).toBe(true) + for (let i = 0; i < DETAILS_ROW_COUNT; i++) { + expect(hasKey(`${PREFIX}.details.row.${i}.label`)).toBe(true) + expect(hasKey(`${PREFIX}.details.row.${i}.value`)).toBe(true) + } + }) + + it('matches every brand-asset tile to a translation key', () => { + expect(hasKey(`${PREFIX}.assets.heading`)).toBe(true) + expect(hasKey(`${PREFIX}.assets.subheading`)).toBe(true) + expect(hasKey(`${PREFIX}.assets.downloadLabel`)).toBe(true) + expect(hasKey(`${PREFIX}.assets.comingSoonLabel`)).toBe(true) + for (const asset of brandAssets) { + expect(hasKey(asset.titleKey)).toBe(true) + } + }) + + // FAQ keys are 1-indexed because that is the naming contract enforced by + // common/FAQSection.vue. Other indexed sections (hero highlights, audience + // items, details rows) are 0-indexed in this file. + it('exposes every faq question/answer pair the FAQSection will render', () => { + expect(AFFILIATE_FAQ_PREFIX).toBe(`${PREFIX}.faq`) + expect(hasKey(AFFILIATE_FAQ_HEADING_KEY)).toBe(true) + for (let n = 1; n <= AFFILIATE_FAQ_COUNT; n++) { + expect(hasKey(`${AFFILIATE_FAQ_PREFIX}.${n}.q`)).toBe(true) + expect(hasKey(`${AFFILIATE_FAQ_PREFIX}.${n}.a`)).toBe(true) + } + }) + + it('exposes the footer cta copy', () => { + expect(hasKey(`${PREFIX}.footerCta.heading`)).toBe(true) + expect(hasKey(`${PREFIX}.footerCta.termsLink`)).toBe(true) + }) + + it('returns non-empty english copy for every affiliate-landing key', () => { + for (const key of affiliateKeys()) { + expect(t(key as never, 'en').trim().length).toBeGreaterThan(0) + } + }) + + it('does not leak internal-only keys (drafts, todos, open questions)', () => { + const leaks = affiliateKeys().filter((k) => + INTERNAL_KEY_PATTERNS.some((re) => re.test(k)) + ) + expect(leaks).toEqual([]) + }) +}) diff --git a/apps/website/src/components/affiliates/brandAssets.ts b/apps/website/src/components/affiliates/brandAssets.ts new file mode 100644 index 0000000000..d7abf35a5a --- /dev/null +++ b/apps/website/src/components/affiliates/brandAssets.ts @@ -0,0 +1,44 @@ +import type { TranslationKey } from '../../i18n/translations' + +interface BrandAsset { + id: string + titleKey: TranslationKey + download?: string + preview?: string + comingSoon?: boolean +} + +export const brandAssets: BrandAsset[] = [ + { + id: 'logo-horizontal', + titleKey: 'affiliate-landing.assets.tile.logo-horizontal.title', + download: '/icons/logo.svg', + preview: '/icons/logo.svg' + }, + { + id: 'logomark', + titleKey: 'affiliate-landing.assets.tile.logomark.title', + download: '/icons/logomark.svg', + preview: '/icons/logomark.svg' + }, + { + id: 'banner-leaderboard', + titleKey: 'affiliate-landing.assets.tile.banner-leaderboard.title', + comingSoon: true + }, + { + id: 'banner-medium-rectangle', + titleKey: 'affiliate-landing.assets.tile.banner-medium-rectangle.title', + comingSoon: true + }, + { + id: 'banner-skyscraper', + titleKey: 'affiliate-landing.assets.tile.banner-skyscraper.title', + comingSoon: true + }, + { + id: 'banner-social', + titleKey: 'affiliate-landing.assets.tile.banner-social.title', + comingSoon: true + } +] diff --git a/apps/website/src/components/common/BrandButton.vue b/apps/website/src/components/common/BrandButton.vue index e905e82f3f..79ad55a01c 100644 --- a/apps/website/src/components/common/BrandButton.vue +++ b/apps/website/src/components/common/BrandButton.vue @@ -9,12 +9,14 @@ import { brandButtonVariants } from './brandButton.variants' const { href, target, + rel, variant, size, class: customClass = '' } = defineProps<{ href?: string target?: string + rel?: string variant?: BrandButtonVariants['variant'] size?: BrandButtonVariants['size'] class?: HTMLAttributes['class'] @@ -26,6 +28,7 @@ const { :is="href ? 'a' : 'button'" :href :target + :rel :class="cn(brandButtonVariants({ variant, size }), customClass)" > diff --git a/apps/website/src/config/routes.ts b/apps/website/src/config/routes.ts index d9b6811e88..3b70057150 100644 --- a/apps/website/src/config/routes.ts +++ b/apps/website/src/config/routes.ts @@ -14,6 +14,8 @@ const baseRoutes = { demos: '/demos', termsOfService: '/terms-of-service', privacyPolicy: '/privacy-policy', + affiliates: '/affiliates', + affiliateTerms: '/affiliates/terms', contact: '/contact' } as const @@ -28,6 +30,7 @@ export function getRoutes(locale: Locale = 'en'): Routes { } export const externalLinks = { + affiliateApplicationForm: 'https://forms.gle/RS8L2ttcuGap4Q1v6', apiKeys: 'https://platform.comfy.org/profile/api-keys', blog: 'https://blog.comfy.org/', cloud: 'https://cloud.comfy.org', diff --git a/apps/website/src/i18n/translations.ts b/apps/website/src/i18n/translations.ts index 419750aa5f..14152194cf 100644 --- a/apps/website/src/i18n/translations.ts +++ b/apps/website/src/i18n/translations.ts @@ -3697,6 +3697,311 @@ const translations = { 'payment.failed.secondaryCta': { en: 'READ SUBSCRIPTION DOCS', 'zh-CN': '查看订阅文档' + }, + + // Affiliate landing page (/affiliates) + // English-only copy; zh-CN values mirror en until a localized version lands. + 'affiliate-landing.page.title': { + en: 'Comfy.org Affiliate Program — Become a Partner', + 'zh-CN': 'Comfy.org Affiliate Program — Become a Partner' + }, + 'affiliate-landing.page.description': { + en: 'Earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer. Apply to become a Comfy Partner.', + 'zh-CN': + 'Earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer. Apply to become a Comfy Partner.' + }, + 'affiliate-landing.cta.apply': { + en: 'Apply Now', + 'zh-CN': 'Apply Now' + }, + 'affiliate-landing.cta.applyAriaLabel': { + en: 'Apply Now (opens in new tab)', + 'zh-CN': 'Apply Now (opens in new tab)' + }, + + // Hero + 'affiliate-landing.hero.heading': { + en: 'Become a Comfy Partner', + 'zh-CN': 'Become a Comfy Partner' + }, + 'affiliate-landing.hero.subheading': { + en: 'Earn 30% Commission for 3 Months.', + 'zh-CN': 'Earn 30% Commission for 3 Months.' + }, + 'affiliate-landing.hero.body': { + en: 'Join the Comfy.org Affiliate Program and earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer:', + 'zh-CN': + 'Join the Comfy.org Affiliate Program and earn 30% recurring commission for 3 months on every Comfy Cloud subscription you refer:' + }, + 'affiliate-landing.hero.highlight.0': { + en: '30% recurring commission for 3 months', + 'zh-CN': '30% recurring commission for 3 months' + }, + 'affiliate-landing.hero.highlight.1': { + en: '60-day cookie window', + 'zh-CN': '60-day cookie window' + }, + 'affiliate-landing.hero.highlight.2': { + en: '$100 minimum payout', + 'zh-CN': '$100 minimum payout' + }, + 'affiliate-landing.hero.highlight.3': { + en: 'Monthly payouts', + 'zh-CN': 'Monthly payouts' + }, + + // Trust band + 'affiliate-landing.trust.label': { + en: 'Trusted by 2M+ creators worldwide', + 'zh-CN': 'Trusted by 2M+ creators worldwide' + }, + + // How it works + 'affiliate-landing.how.heading': { + en: 'How it works', + 'zh-CN': 'How it works' + }, + 'affiliate-landing.how.step.0.title': { + en: 'Apply.', + 'zh-CN': 'Apply.' + }, + 'affiliate-landing.how.step.0.body': { + en: 'Submit a quick form. Most applicants approved same day.', + 'zh-CN': 'Submit a quick form. Most applicants approved same day.' + }, + 'affiliate-landing.how.step.1.title': { + en: 'Share.', + 'zh-CN': 'Share.' + }, + 'affiliate-landing.how.step.1.body': { + en: 'Get your unique tracking link. Share via content, social, email, however you reach your audience.', + 'zh-CN': + 'Get your unique tracking link. Share via content, social, email, however you reach your audience.' + }, + 'affiliate-landing.how.step.2.title': { + en: 'Earn.', + 'zh-CN': 'Earn.' + }, + 'affiliate-landing.how.step.2.body': { + en: '30% recurring commission for 3 months on every Comfy Cloud subscriber you refer. Tracked in real-time. Paid monthly.', + 'zh-CN': + '30% recurring commission for 3 months on every Comfy Cloud subscriber you refer. Tracked in real-time. Paid monthly.' + }, + + // Who we're looking for + 'affiliate-landing.audience.heading': { + en: "Who we're looking for", + 'zh-CN': "Who we're looking for" + }, + 'affiliate-landing.audience.item.0': { + en: 'ComfyUI tutorial creators and workflow builders', + 'zh-CN': 'ComfyUI tutorial creators and workflow builders' + }, + 'affiliate-landing.audience.item.1': { + en: 'AI tool reviewers on YouTube, TikTok, blogs', + 'zh-CN': 'AI tool reviewers on YouTube, TikTok, blogs' + }, + 'affiliate-landing.audience.item.2': { + en: 'Tech bloggers covering AI creative tools', + 'zh-CN': 'Tech bloggers covering AI creative tools' + }, + 'affiliate-landing.audience.item.3': { + en: 'Newsletter operators in the AI/creative space', + 'zh-CN': 'Newsletter operators in the AI/creative space' + }, + 'affiliate-landing.audience.item.4': { + en: 'Anyone with an audience interested in AI image, video, or 3D generation', + 'zh-CN': + 'Anyone with an audience interested in AI image, video, or 3D generation' + }, + + // Program details + 'affiliate-landing.details.heading': { + en: 'Program details', + 'zh-CN': 'Program details' + }, + 'affiliate-landing.details.headerLabel': { + en: 'Detail', + 'zh-CN': 'Detail' + }, + 'affiliate-landing.details.headerValue': { + en: 'Value', + 'zh-CN': 'Value' + }, + 'affiliate-landing.details.row.0.label': { + en: 'Commission rate', + 'zh-CN': 'Commission rate' + }, + 'affiliate-landing.details.row.0.value': { + en: '30% recurring', + 'zh-CN': '30% recurring' + }, + 'affiliate-landing.details.row.1.label': { + en: 'Commission duration', + 'zh-CN': 'Commission duration' + }, + 'affiliate-landing.details.row.1.value': { + en: '3 months', + 'zh-CN': '3 months' + }, + 'affiliate-landing.details.row.2.label': { + en: 'Cookie window', + 'zh-CN': 'Cookie window' + }, + 'affiliate-landing.details.row.2.value': { + en: '60 days', + 'zh-CN': '60 days' + }, + 'affiliate-landing.details.row.3.label': { + en: 'Eligible products', + 'zh-CN': 'Eligible products' + }, + 'affiliate-landing.details.row.3.value': { + en: 'Comfy Cloud paid subscription plans', + 'zh-CN': 'Comfy Cloud paid subscription plans' + }, + 'affiliate-landing.details.row.4.label': { + en: 'Payouts', + 'zh-CN': 'Payouts' + }, + 'affiliate-landing.details.row.4.value': { + en: 'Monthly, within first 10 business days', + 'zh-CN': 'Monthly, within first 10 business days' + }, + 'affiliate-landing.details.row.5.label': { + en: 'Minimum payout', + 'zh-CN': 'Minimum payout' + }, + 'affiliate-landing.details.row.5.value': { + en: '$100', + 'zh-CN': '$100' + }, + + // Brand assets + 'affiliate-landing.assets.heading': { + en: 'Brand assets', + 'zh-CN': 'Brand assets' + }, + 'affiliate-landing.assets.subheading': { + en: 'Logos and banners for your content. More assets in your affiliate dashboard after approval.', + 'zh-CN': + 'Logos and banners for your content. More assets in your affiliate dashboard after approval.' + }, + 'affiliate-landing.assets.downloadLabel': { + en: 'Download', + 'zh-CN': 'Download' + }, + 'affiliate-landing.assets.comingSoonLabel': { + en: 'Coming soon', + 'zh-CN': 'Coming soon' + }, + 'affiliate-landing.assets.tile.logo-horizontal.title': { + en: 'Comfy logo (horizontal)', + 'zh-CN': 'Comfy logo (horizontal)' + }, + 'affiliate-landing.assets.tile.logomark.title': { + en: 'Comfy logomark', + 'zh-CN': 'Comfy logomark' + }, + 'affiliate-landing.assets.tile.banner-leaderboard.title': { + en: 'Banner — 728×90', + 'zh-CN': 'Banner — 728×90' + }, + 'affiliate-landing.assets.tile.banner-medium-rectangle.title': { + en: 'Banner — 300×250', + 'zh-CN': 'Banner — 300×250' + }, + 'affiliate-landing.assets.tile.banner-skyscraper.title': { + en: 'Banner — 160×600', + 'zh-CN': 'Banner — 160×600' + }, + 'affiliate-landing.assets.tile.banner-social.title': { + en: 'Social card — 1200×628', + 'zh-CN': 'Social card — 1200×628' + }, + + // FAQ — keys follow the FAQSection contract: ..q / ..a, 1-indexed + 'affiliate-landing.faq.heading': { + en: 'Questions', + 'zh-CN': 'Questions' + }, + 'affiliate-landing.faq.1.q': { + en: 'How do I track my referrals?', + 'zh-CN': 'How do I track my referrals?' + }, + 'affiliate-landing.faq.1.a': { + en: 'Real-time dashboard via our partner portal.', + 'zh-CN': 'Real-time dashboard via our partner portal.' + }, + 'affiliate-landing.faq.2.q': { + en: 'What plans qualify?', + 'zh-CN': 'What plans qualify?' + }, + 'affiliate-landing.faq.2.a': { + en: 'All Comfy Cloud paid subscription plans (Creator, Pro, Teams).', + 'zh-CN': 'All Comfy Cloud paid subscription plans (Creator, Pro, Teams).' + }, + 'affiliate-landing.faq.3.q': { + en: 'How long does approval take?', + 'zh-CN': 'How long does approval take?' + }, + 'affiliate-landing.faq.3.a': { + en: 'Most applications approved within 24 hours.', + 'zh-CN': 'Most applications approved within 24 hours.' + }, + 'affiliate-landing.faq.4.q': { + en: 'When do I get paid?', + 'zh-CN': 'When do I get paid?' + }, + 'affiliate-landing.faq.4.a': { + en: 'Monthly, within the first 10 business days. Minimum balance $100. Paid via Stripe Express or PayPal.', + 'zh-CN': + 'Monthly, within the first 10 business days. Minimum balance $100. Paid via Stripe Express or PayPal.' + }, + 'affiliate-landing.faq.5.q': { + en: 'What happens if my referral upgrades or downgrades?', + 'zh-CN': 'What happens if my referral upgrades or downgrades?' + }, + 'affiliate-landing.faq.5.a': { + en: 'If they upgrade, your commission increases. If they downgrade, it adjusts accordingly. Commission is based on actual amounts received by Comfy.org, net of refunds.', + 'zh-CN': + 'If they upgrade, your commission increases. If they downgrade, it adjusts accordingly. Commission is based on actual amounts received by Comfy.org, net of refunds.' + }, + 'affiliate-landing.faq.6.q': { + en: 'Can I use coupon codes?', + 'zh-CN': 'Can I use coupon codes?' + }, + 'affiliate-landing.faq.6.a': { + en: 'Yes. We support both tracking links and unique coupon codes.', + 'zh-CN': 'Yes. We support both tracking links and unique coupon codes.' + }, + 'affiliate-landing.faq.7.q': { + en: 'What if my referral uses an ad blocker?', + 'zh-CN': 'What if my referral uses an ad blocker?' + }, + 'affiliate-landing.faq.7.a': { + en: 'We use server-side tracking, so conversions are tracked regardless.', + 'zh-CN': + 'We use server-side tracking, so conversions are tracked regardless.' + }, + 'affiliate-landing.faq.8.q': { + en: 'What assets do you provide?', + 'zh-CN': 'What assets do you provide?' + }, + 'affiliate-landing.faq.8.a': { + en: 'Logos and banners on this page, plus screenshots and talking points in your affiliate dashboard after approval.', + 'zh-CN': + 'Logos and banners on this page, plus screenshots and talking points in your affiliate dashboard after approval.' + }, + + // Footer CTA + 'affiliate-landing.footerCta.heading': { + en: 'Ready to start earning?', + 'zh-CN': 'Ready to start earning?' + }, + 'affiliate-landing.footerCta.termsLink': { + en: 'Read the affiliate program terms', + 'zh-CN': 'Read the affiliate program terms' } } as const satisfies Record> diff --git a/apps/website/src/pages/affiliates/index.astro b/apps/website/src/pages/affiliates/index.astro new file mode 100644 index 0000000000..9e7acf4a54 --- /dev/null +++ b/apps/website/src/pages/affiliates/index.astro @@ -0,0 +1,66 @@ +--- +import BaseLayout from '../../layouts/BaseLayout.astro' +import AudienceSection from '../../components/affiliates/AudienceSection.vue' +import BrandAssetsSection from '../../components/affiliates/BrandAssetsSection.vue' +import FooterCtaSection from '../../components/affiliates/FooterCtaSection.vue' +import HeroSection from '../../components/affiliates/HeroSection.vue' +import HowItWorksSection from '../../components/affiliates/HowItWorksSection.vue' +import ProgramDetailsSection from '../../components/affiliates/ProgramDetailsSection.vue' +import TrustBandSection from '../../components/affiliates/TrustBandSection.vue' +import FAQSection from '../../components/common/FAQSection.vue' +import { + AFFILIATE_FAQ_COUNT, + AFFILIATE_FAQ_HEADING_KEY, + AFFILIATE_FAQ_PREFIX +} from '../../components/affiliates/affiliateFaqs' +import type { Locale, TranslationKey } from '../../i18n/translations' +import { t } from '../../i18n/translations' + +const locale: Locale = + Astro.currentLocale === 'zh-CN' ? 'zh-CN' : 'en' + +const faqJsonLd = { + '@context': 'https://schema.org', + '@type': 'FAQPage', + mainEntity: Array.from({ length: AFFILIATE_FAQ_COUNT }, (_, i) => { + const n = i + 1 + return { + '@type': 'Question', + name: t(`${AFFILIATE_FAQ_PREFIX}.${n}.q` as TranslationKey, locale), + acceptedAnswer: { + '@type': 'Answer', + text: t(`${AFFILIATE_FAQ_PREFIX}.${n}.a` as TranslationKey, locale) + } + } + }) +} +--- + + + +