openvibers/openvibe.host Commits
Catalog: a plan page carries one Product with its monthly price as a number (SEO-10) (#189)
Changes
@@@ -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,}),];},@@@ -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);});
@@ -301,13 +301,17 @@ const jsonLd = {
@@ -301,13 +301,17 @@ const jsonLd = {
},
},
plan(base, p) {
plan(base, p) {
const price = Number(p.price_monthly);
const price = Number(p.price_monthly);
+ const url = planUrl(base, p);
return [
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({
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,
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,
}),
}),
];
];
},
},
@@ -59,7 +59,7 @@ const { boot, check, done } = require('./helpers/boot');
@@ -59,7 +59,7 @@ const { boot, check, done } = require('./helpers/boot');
const product = ld(planPage.text).find((x) => x['@type'] === 'Product');
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, /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.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');
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);
const promos = await t.api('GET', '/api/v1/catalog/promos'); assert.strictEqual(promos.json().promos.length, 1);
});
});