feat(website): add education offers and FAQ page JSON-LD builders

This commit is contained in:
Mobeen Abdullah
2026-07-17 02:21:54 +05:00
parent e9eac5ccce
commit 8bafafbf45
4 changed files with 82 additions and 5 deletions

View File

@@ -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))
})
})
})

View File

@@ -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)
}

View File

@@ -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<string, unknown>[]
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)

View File

@@ -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