From 8bafafbf451d6bbac3475cabde36213b69bcbb0e Mon Sep 17 00:00:00 2001 From: Mobeen Abdullah Date: Fri, 17 Jul 2026 02:21:54 +0500 Subject: [PATCH] feat(website): add education offers and FAQ page JSON-LD builders --- apps/website/src/config/pricing.test.ts | 21 ++++++++++++++++++++ apps/website/src/config/pricing.ts | 26 ++++++++++++++++++++----- apps/website/src/utils/jsonLd.test.ts | 20 +++++++++++++++++++ apps/website/src/utils/jsonLd.ts | 20 +++++++++++++++++++ 4 files changed, 82 insertions(+), 5 deletions(-) create mode 100644 apps/website/src/config/pricing.test.ts diff --git a/apps/website/src/config/pricing.test.ts b/apps/website/src/config/pricing.test.ts new file mode 100644 index 0000000000..70fe58774e --- /dev/null +++ b/apps/website/src/config/pricing.test.ts @@ -0,0 +1,21 @@ +import { describe, expect, it } from 'vitest' + +import { educationOffers, pricingOffers } from './pricing' + +describe('educationOffers', () => { + it('marks up the discounted education prices, not the list prices', () => { + const edu = educationOffers('en') + const list = pricingOffers('en') + + // Same plans in the same order, so a per-tier comparison is meaningful. + expect(edu.map((offer) => offer.name)).toEqual( + list.map((offer) => offer.name) + ) + expect(edu.length).toBeGreaterThan(0) + + edu.forEach((offer, index) => { + expect(offer.price).toMatch(/^\d+(\.\d+)?$/) + expect(Number(offer.price)).toBeLessThan(Number(list[index].price)) + }) + }) +}) diff --git a/apps/website/src/config/pricing.ts b/apps/website/src/config/pricing.ts index adbf44fb66..ea6df7221a 100644 --- a/apps/website/src/config/pricing.ts +++ b/apps/website/src/config/pricing.ts @@ -6,23 +6,27 @@ interface PricingTier { slug: string labelKey: TranslationKey priceKey: TranslationKey + eduPriceKey: TranslationKey } const tiers: PricingTier[] = [ { slug: 'standard', labelKey: 'pricing.plan.standard.label', - priceKey: 'pricing.plan.standard.price' + priceKey: 'pricing.plan.standard.price', + eduPriceKey: 'pricing.plan.standard.eduPrice' }, { slug: 'creator', labelKey: 'pricing.plan.creator.label', - priceKey: 'pricing.plan.creator.price' + priceKey: 'pricing.plan.creator.price', + eduPriceKey: 'pricing.plan.creator.eduPrice' }, { slug: 'pro', labelKey: 'pricing.plan.pro.label', - priceKey: 'pricing.plan.pro.price' + priceKey: 'pricing.plan.pro.price', + eduPriceKey: 'pricing.plan.pro.eduPrice' } ] @@ -32,9 +36,13 @@ export interface PricingOffer { url: string } -export function pricingOffers(locale: Locale): PricingOffer[] { +// Shared offer builder: the pricing page marks up list prices, the education +// page marks up the discounted student/educator prices. Only plain USD amounts +// become offers so the JSON-LD Offer always carries a concrete numeric price. +function offersFrom(locale: Locale, education: boolean): PricingOffer[] { return tiers.flatMap((tier) => { - const display = t(tier.priceKey, locale).trim() + const priceKey = education ? tier.eduPriceKey : tier.priceKey + const display = t(priceKey, locale).trim() const match = /^\$(\d+(?:\.\d+)?)$/.exec(display) if (!match) { console.warn( @@ -51,3 +59,11 @@ export function pricingOffers(locale: Locale): PricingOffer[] { ] }) } + +export function pricingOffers(locale: Locale): PricingOffer[] { + return offersFrom(locale, false) +} + +export function educationOffers(locale: Locale): PricingOffer[] { + return offersFrom(locale, true) +} diff --git a/apps/website/src/utils/jsonLd.test.ts b/apps/website/src/utils/jsonLd.test.ts index e201fe0d71..7df1c329e0 100644 --- a/apps/website/src/utils/jsonLd.test.ts +++ b/apps/website/src/utils/jsonLd.test.ts @@ -11,6 +11,7 @@ import { comfyUiApplicationNode, comfyUiSoftwareId, comfyUiSourceCodeNode, + faqPageNode, itemListNode, jsonLdId, organizationId, @@ -201,6 +202,25 @@ describe('productNode', () => { }) }) +describe('faqPageNode', () => { + const pageUrl = 'https://comfy.org/education/' + + it('maps each FAQ to a Question with an accepted answer', () => { + const node = faqPageNode(pageUrl, [ + { question: 'What discount do I get?', answer: 'Up to 25% off.' } + ]) + expect(node['@type']).toBe('FAQPage') + expect(node['@id']).toBe(jsonLdId(pageUrl, 'faq')) + const questions = node.mainEntity as Record[] + expect(questions).toHaveLength(1) + expect(questions[0]).toMatchObject({ + '@type': 'Question', + name: 'What discount do I get?', + acceptedAnswer: { '@type': 'Answer', text: 'Up to 25% off.' } + }) + }) +}) + describe('comfyUiSourceCodeNode', () => { it('links the source code to the ComfyUI application via targetProduct', () => { const node = comfyUiSourceCodeNode(siteUrl) diff --git a/apps/website/src/utils/jsonLd.ts b/apps/website/src/utils/jsonLd.ts index 93a3ab4ae6..6192251d4f 100644 --- a/apps/website/src/utils/jsonLd.ts +++ b/apps/website/src/utils/jsonLd.ts @@ -347,6 +347,26 @@ export function productNode(input: ProductInput): JsonLdNode { } } +export interface FaqEntry { + question: string + answer: string +} + +export function faqPageNode(pageUrl: string, faqs: FaqEntry[]): JsonLdNode { + return { + '@type': 'FAQPage', + '@id': jsonLdId(pageUrl, 'faq'), + mainEntity: faqs.map((faq) => ({ + '@type': 'Question', + name: faq.question, + acceptedAnswer: { + '@type': 'Answer', + text: faq.answer + } + })) + } +} + export interface PageGraphInput { url: string name: string