openvibers/openvibe.host Commits

Catalog: a plan page carries one Product with its monthly price as a number (SEO-10) (#189)

OP OpenVibers committed 15 days ago · 901c02a7f1 · 2 files, +9 −5

Changes

ddiff --git a/server/render/catalog.js b/server/render/catalog.js
iindex 0175aa4..8c60d21 100644
---- a/server/render/catalog.js
++++ b/server/render/catalog.js
@@@ -301,13 +301,17 @@ const jsonLd = {
},
plan(base, p) {
const price = Number(p.price_monthly);
++ const url = planUrl(base, p);
return [
-- seo.jsonLd.breadcrumbs([{ name: 'Catalog', url: `${base}/catalog` }, { name: p.provider_name, url: `${base}/catalog/providers/${encodeURIComponent(p.provider_slug)}` }, { name: p.name, url: planUrl(base, p) }]),
++ seo.jsonLd.breadcrumbs([{ name: 'Catalog', url: `${base}/catalog` }, { name: p.provider_name, url: `${base}/catalog/providers/${encodeURIComponent(p.provider_slug)}` }, { name: p.name, url }]),
++ // One Product node for the plan's own page: the page's own heading is the plan's name,
++ // the page names the provider (so it is the brand), and the price it shows is the monthly
++ // price as a number. The plan is on sale only while it is active, so that is its availability.
seo.jsonLd.product({
-- name: `${p.provider_name} ${p.name}`, url: planUrl(base, p), brand: { '@type': 'Brand', name: p.provider_name },
++ name: p.name, url, brand: { '@type': 'Brand', name: p.provider_name },
description: [p.vcpu != null ? `${Number(p.vcpu)} vCPU` : null, bytesMb(p.ram_mb) ? `${bytesMb(p.ram_mb)} RAM` : null, sizeGb(p.disk_gb) ? `${sizeGb(p.disk_gb)} disk` : null].filter(Boolean).join(', ') || undefined,
-- offers: Number.isFinite(price) ? { '@type': 'Offer', price: price.toFixed(2), priceCurrency: p.currency, url: p.source_url, availability: 'https://schema.org/InStock',
-- priceSpecification: { '@type': 'UnitPriceSpecification', price: price.toFixed(2), priceCurrency: p.currency, unitText: 'MONTH' } } : undefined,
++ offers: Number.isFinite(price) ? { '@type': 'Offer', price, priceCurrency: p.currency, url, availability: 'https://schema.org/InStock',
++ priceSpecification: { '@type': 'UnitPriceSpecification', price, priceCurrency: p.currency, unitText: 'MONTH' } } : undefined,
}),
];
},
ddiff --git a/test/catalog-public.test.js b/test/catalog-public.test.js
iindex 7c190fe..5f523b1 100644
---- a/test/catalog-public.test.js
++++ b/test/catalog-public.test.js
@@@ -59,7 +59,7 @@ const { boot, check, done } = require('./helpers/boot');
const product = ld(planPage.text).find((x) => x['@type'] === 'Product');
assert.match(planPage.text, /class="hc-visit"/, 'a plan page offers a way to the provider');
assert.match(planPage.text, /href="https:\/\/openvibe\.watch\/watches\/new\?template=json&url=[^"]*%2Fapi%2Fv1%2Fcatalog%2Fplans%2F[^"]*value_path=plan\.price_monthly&op=changed/, 'a plan page offers a prefilled price watch');
-- assert.ok(product && product.offers.price === '4.00' && product.offers.priceCurrency === 'USD' && product.offers.priceSpecification.unitText === 'MONTH');
++ assert.ok(product && product.offers.price === 4 && product.offers.priceCurrency === 'USD' && product.offers.priceSpecification.unitText === 'MONTH');
assert.ok(!types((await t.api('GET', '/catalog?min_vcpu=2')).text).includes('ItemList'), 'a filtered view carries no JSON-LD');
const promos = await t.api('GET', '/api/v1/catalog/promos'); assert.strictEqual(promos.json().promos.length, 1);
});