openvibers/openvibe.deals Commits
Deals: one JSON-LD graph per page, the price as a number, and the offer's own heading as its name (SEO-10) (#131)
Changes
@@@ -175,7 +175,8 @@ function createPublication({ config, store, reads, catalog }) {seller: v.root.seller ? { '@type': 'Organization', name: v.root.seller } : v.store ? { '@type': 'Organization', name: v.store.name || v.store.domain } : undefined,};if (fresh && o) {-- if (o.price != null && o.currency) { ld.price = o.price; ld.priceCurrency = o.currency; }++ // The price a buyer pays, as a number (the page shows the same number and its currency).++ if (o.price != null && o.currency) { ld.price = o.price_num != null ? o.price_num : Number(o.price); ld.priceCurrency = o.currency; }if (o.availability) ld.availability = `https://schema.org/${{ in_stock: 'InStock', out_of_stock: 'OutOfStock', preorder: 'PreOrder', discontinued: 'Discontinued', limited: 'LimitedAvailability', sold_out: 'SoldOut', online_only: 'OnlineOnly', in_store_only: 'InStoreOnly' }[o.availability]}`;if (o.condition) ld.itemCondition = `https://schema.org/${{ new: 'NewCondition', used: 'UsedCondition', refurbished: 'RefurbishedCondition', damaged: 'DamagedCondition' }[o.condition]}`;if (o.shipping != null && o.currency) ld.shippingDetails = { '@type': 'OfferShippingDetails', shippingRate: { '@type': 'MonetaryAmount', value: o.shipping, currency: o.currency } };@@@ -184,12 +185,14 @@ function createPublication({ config, store, reads, catalog }) {return ld;}++ /**++ * The offer's own page: one Product node, named by the page's own heading (the offer's title).++ * The page never names a brand, so no brand is claimed; the brand belongs to the product's own page.++ */function offerJsonLd(v) {-- const name = v.product ? v.product.name : v.root.title;return seo.compact({-- '@context': 'https://schema.org', '@type': 'Product', name, url: abs(offerPath(v.root)),++ '@context': 'https://schema.org', '@type': 'Product', name: v.root.title, url: abs(offerPath(v.root)),description: v.root.description || undefined,-- brand: v.product && v.product.brand ? { '@type': 'Brand', name: v.product.brand } : undefined,offers: [offerLd(v)],});}@@@ -31,7 +31,8 @@ const frame = require('openvibe-shared/frame');const seo = require('openvibe-publishing/seo');const cache = require('openvibe-shared/cache-policy');const ssr = require('openvibe-publishing/ssr');--const { renderPage, SITE_SUMMARY } = require('../render/layout');++const { renderPage, SITE_SUMMARY, oneGraph } = require('../render/layout');++const sharedSeo = require('openvibe-shared/seo');const views = require('../render/views');const home = require('../render/home');const { csrfToken, checkCsrf } = require('../auth/forms');@@@ -205,11 +206,14 @@ function createPages(ctx) {decision: pageDecision('/', { empty: total === 0 }),canonical: seo.canonicalUrl(config.baseUrl, '/'),feeds: FEEDS,-- jsonLd: [-- { '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Deals', url: publication.abs('/'), potentialAction: { '@type': 'SearchAction', target: `${publication.abs('/search')}?q={q}`, 'query-input': 'required name=q' } },-- ...(listed.length ? [{ '@context': 'https://schema.org', '@type': 'ItemList', name: 'Deals on OpenVibe.Deals', itemListElement: listed.map((v, i) => ({ '@type': 'ListItem', position: i + 1, url: publication.abs(publication.offerPath(v.root)), name: v.root.title })) }] : []),++ // One JSON-LD script for the page: every node in one @graph.++ jsonLd: oneGraph([++ { '@type': 'WebSite', name: 'OpenVibe.Deals', url: publication.abs('/'), potentialAction: { '@type': 'SearchAction', target: `${publication.abs('/search')}?q={q}`, 'query-input': 'required name=q' } },++ ...(listed.length ? [{ '@type': 'ItemList', name: 'Deals on OpenVibe.Deals', itemListElement: listed.map((v, i) => ({ '@type': 'ListItem', position: i + 1, url: publication.abs(publication.offerPath(v.root)), name: v.root.title })) }] : []),home.faqJsonLd(faq),-- ],++ // The publisher of the WebSite above, so its @id resolves on this page too.++ sharedSeo.jsonLd.organization(),++ ]),bodyClass: 'home',body: home.homePage({empty: total === 0, freshnessHours: Math.round(config.freshnessMs / 3600000), faq,@@@ -281,7 +285,7 @@ function createPages(ctx) {decision: v.decision,canonical,type: 'website',-- jsonLd: [publication.offerJsonLd(v), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, ...(v.store ? [{ name: v.store.name || v.store.domain, url: publication.abs(publication.storePath(v.store)) }] : []), { name: r.title, url: canonical }])],++ jsonLd: oneGraph([publication.offerJsonLd(v), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, ...(v.store ? [{ name: v.store.name || v.store.domain, url: publication.abs(publication.storePath(v.store)) }] : []), { name: r.title, url: canonical }])]),body: views.offerPage({v, dto: publication.offerDto(v), viewer, csrf: person ? csrfToken(config, viewer) : '', myVote: person ? await votes.mine(viewer.subject, r) : 0,comments: c, mergedComments, canEdit: access.canEdit(viewer, r), isMod,@@@ -388,7 +392,7 @@ function createPages(ctx) {title: `${product.name} — price comparison`,description: `Offers for ${product.name}, each with the time its price was observed.`,decision: pv.decision, canonical,-- jsonLd: [publication.productJsonLd(pv), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, { name: product.name, url: canonical }])],++ jsonLd: oneGraph([publication.productJsonLd(pv), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, { name: product.name, url: canonical }])]),body: views.productPage({ pv, urls }),},};@@@ -15,6 +15,15 @@ const path = require('path');const layout = require('openvibe-publishing/layout');const frame = require('openvibe-shared/frame');++/**++ * The page's JSON-LD in one script: every node in one @graph, so a page carries exactly one JSON-LD script.++ * The nodes keep their own fields; only their own @context goes, because the graph carries it once.++ */++const oneGraph = (nodes) => {++ const graph = nodes.filter(Boolean).map((n) => { const c = { ...n }; delete c['@context']; return c; });++ return [{ '@context': 'https://schema.org', '@graph': graph }];++};++const NETWORK_URL = 'https://openvibe.network';const SITE_NAME = 'OpenVibe.Deals';// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.@@@ -113,4 +122,4 @@ function renderPage(o) {});}--module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, SITE_SUMMARY, NETWORK_URL };++module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, SITE_SUMMARY, NETWORK_URL, oneGraph };@@@ -20,7 +20,7 @@ const { sourceItem } = require('./helpers/mocks');assert.match(page.text, /data-freshness="fresh"/);assert.strictEqual(robotsOf(page.text), 'index, follow');const ld = jsonLd(page.text).find((x) => x['@type'] === 'Product');-- assert.strictEqual(ld.offers[0].price, '49.99');++ assert.strictEqual(ld.offers[0].price, 49.99);assert.strictEqual(ld.offers[0].priceCurrency, 'USD');assert.strictEqual(ld.offers[0].availability, 'https://schema.org/InStock');const json = await t.offerJson(offer.slug);@@@ -82,7 +82,7 @@ const { sourceItem } = require('./helpers/mocks');assert.strictEqual(json.observations[0].observed_by, bob.subject);const page = await t.get(`/d/${offer.slug}`);assert.strictEqual(robotsOf(page.text), 'index, follow');-- assert.strictEqual(jsonLd(page.text).find((x) => x['@type'] === 'Product').offers[0].price, '44.00');++ assert.strictEqual(jsonLd(page.text).find((x) => x['@type'] === 'Product').offers[0].price, 44);});await check('an imported observation is dated by when the source saw it: old retrieval → stale from the start', async () => {@@@ -104,8 +104,15 @@ async function boot(opts = {}) {}/** JSON-LD blocks of an HTML page. */++/** The JSON-LD of a page: every script's nodes, with a one-@graph script flattened into its nodes. */function jsonLd(htmlText) {-- return [...htmlText.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));++ return [...htmlText.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)]++ .flatMap((m) => { const doc = JSON.parse(m[1]); return Array.isArray(doc['@graph']) ? doc['@graph'] : [doc]; });++}++++/** How many JSON-LD scripts a page carries (the estate ships one per page). */++function jsonLdScripts(htmlText) {++ return [...htmlText.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].length;}const robotsOf = (htmlText) => (/<meta name="robots" content="([^"]+)"/.exec(htmlText) || [])[1];@@@ -116,4 +123,4 @@ async function check(name, fn) {}function done() { console.log(failures ? `\n${failures} failed` : '\nall passed'); process.exit(failures ? 1 : 0); }--module.exports = { boot, check, done, makeClock, jsonLd, robotsOf, HOUR };++module.exports = { boot, check, done, makeClock, jsonLd, jsonLdScripts, robotsOf, HOUR };@@@ -60,7 +60,7 @@ const { sourceItem } = require('./helpers/mocks');const ld = jsonLd(page.text).find((x) => x['@type'] === 'Product');assert.strictEqual(ld.gtin, '0012345678905');const prices = ld.offers.map((o) => o.price);-- assert.deepStrictEqual(prices, ['30.00', undefined, undefined], 'only the fresh stated price reaches structured data');++ assert.deepStrictEqual(prices, [30, undefined, undefined], 'only the fresh stated price reaches structured data');});await check('a price in an imported headline is not parsed into a price', async () => {@@@ -65,7 +65,7 @@ const { boot, check, done, robotsOf, jsonLd, HOUR } = require('./helpers/boot');const types = jsonLd(r.text).map((x) => x['@type']);assert.deepStrictEqual(types, ['Product', 'BreadcrumbList']);const ld = jsonLd(r.text)[0].offers[0];-- assert.deepStrictEqual([ld.price, ld.priceCurrency, ld.itemCondition, ld.seller.name], ['79.00', 'USD', 'https://schema.org/NewCondition', 'Gadgets Inc']);++ assert.deepStrictEqual([ld.price, ld.priceCurrency, ld.itemCondition, ld.seller.name], [79, 'USD', 'https://schema.org/NewCondition', 'Gadgets Inc']);assert.strictEqual(ld.shippingDetails.shippingRate.value, '4.99');assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe.deals\/d\//);const json = await t.get(`/d/${deal.slug}.json`);
@@ -175,7 +175,8 @@ function createPublication({ config, store, reads, catalog }) {
@@ -175,7 +175,8 @@ function createPublication({ config, store, reads, catalog }) {
seller: v.root.seller ? { '@type': 'Organization', name: v.root.seller } : v.store ? { '@type': 'Organization', name: v.store.name || v.store.domain } : undefined,
seller: v.root.seller ? { '@type': 'Organization', name: v.root.seller } : v.store ? { '@type': 'Organization', name: v.store.name || v.store.domain } : undefined,
};
};
if (fresh && o) {
if (fresh && o) {
- if (o.price != null && o.currency) { ld.price = o.price; ld.priceCurrency = o.currency; }
+ // The price a buyer pays, as a number (the page shows the same number and its currency).
+ if (o.price != null && o.currency) { ld.price = o.price_num != null ? o.price_num : Number(o.price); ld.priceCurrency = o.currency; }
if (o.availability) ld.availability = `https://schema.org/${{ in_stock: 'InStock', out_of_stock: 'OutOfStock', preorder: 'PreOrder', discontinued: 'Discontinued', limited: 'LimitedAvailability', sold_out: 'SoldOut', online_only: 'OnlineOnly', in_store_only: 'InStoreOnly' }[o.availability]}`;
if (o.availability) ld.availability = `https://schema.org/${{ in_stock: 'InStock', out_of_stock: 'OutOfStock', preorder: 'PreOrder', discontinued: 'Discontinued', limited: 'LimitedAvailability', sold_out: 'SoldOut', online_only: 'OnlineOnly', in_store_only: 'InStoreOnly' }[o.availability]}`;
if (o.condition) ld.itemCondition = `https://schema.org/${{ new: 'NewCondition', used: 'UsedCondition', refurbished: 'RefurbishedCondition', damaged: 'DamagedCondition' }[o.condition]}`;
if (o.condition) ld.itemCondition = `https://schema.org/${{ new: 'NewCondition', used: 'UsedCondition', refurbished: 'RefurbishedCondition', damaged: 'DamagedCondition' }[o.condition]}`;
if (o.shipping != null && o.currency) ld.shippingDetails = { '@type': 'OfferShippingDetails', shippingRate: { '@type': 'MonetaryAmount', value: o.shipping, currency: o.currency } };
if (o.shipping != null && o.currency) ld.shippingDetails = { '@type': 'OfferShippingDetails', shippingRate: { '@type': 'MonetaryAmount', value: o.shipping, currency: o.currency } };
@@ -184,12 +185,14 @@ function createPublication({ config, store, reads, catalog }) {
@@ -184,12 +185,14 @@ function createPublication({ config, store, reads, catalog }) {
return ld;
return ld;
}
}
+ /**
+ * The offer's own page: one Product node, named by the page's own heading (the offer's title).
+ * The page never names a brand, so no brand is claimed; the brand belongs to the product's own page.
+ */
function offerJsonLd(v) {
function offerJsonLd(v) {
- const name = v.product ? v.product.name : v.root.title;
return seo.compact({
return seo.compact({
- '@context': 'https://schema.org', '@type': 'Product', name, url: abs(offerPath(v.root)),
+ '@context': 'https://schema.org', '@type': 'Product', name: v.root.title, url: abs(offerPath(v.root)),
description: v.root.description || undefined,
description: v.root.description || undefined,
- brand: v.product && v.product.brand ? { '@type': 'Brand', name: v.product.brand } : undefined,
offers: [offerLd(v)],
offers: [offerLd(v)],
});
});
}
}
@@ -31,7 +31,8 @@ const frame = require('openvibe-shared/frame');
@@ -31,7 +31,8 @@ const frame = require('openvibe-shared/frame');
const seo = require('openvibe-publishing/seo');
const seo = require('openvibe-publishing/seo');
const cache = require('openvibe-shared/cache-policy');
const cache = require('openvibe-shared/cache-policy');
const ssr = require('openvibe-publishing/ssr');
const ssr = require('openvibe-publishing/ssr');
-const { renderPage, SITE_SUMMARY } = require('../render/layout');
+const { renderPage, SITE_SUMMARY, oneGraph } = require('../render/layout');
+const sharedSeo = require('openvibe-shared/seo');
const views = require('../render/views');
const views = require('../render/views');
const home = require('../render/home');
const home = require('../render/home');
const { csrfToken, checkCsrf } = require('../auth/forms');
const { csrfToken, checkCsrf } = require('../auth/forms');
@@ -205,11 +206,14 @@ function createPages(ctx) {
@@ -205,11 +206,14 @@ function createPages(ctx) {
decision: pageDecision('/', { empty: total === 0 }),
decision: pageDecision('/', { empty: total === 0 }),
canonical: seo.canonicalUrl(config.baseUrl, '/'),
canonical: seo.canonicalUrl(config.baseUrl, '/'),
feeds: FEEDS,
feeds: FEEDS,
- jsonLd: [
- { '@context': 'https://schema.org', '@type': 'WebSite', name: 'OpenVibe.Deals', url: publication.abs('/'), potentialAction: { '@type': 'SearchAction', target: `${publication.abs('/search')}?q={q}`, 'query-input': 'required name=q' } },
- ...(listed.length ? [{ '@context': 'https://schema.org', '@type': 'ItemList', name: 'Deals on OpenVibe.Deals', itemListElement: listed.map((v, i) => ({ '@type': 'ListItem', position: i + 1, url: publication.abs(publication.offerPath(v.root)), name: v.root.title })) }] : []),
+ // One JSON-LD script for the page: every node in one @graph.
+ jsonLd: oneGraph([
+ { '@type': 'WebSite', name: 'OpenVibe.Deals', url: publication.abs('/'), potentialAction: { '@type': 'SearchAction', target: `${publication.abs('/search')}?q={q}`, 'query-input': 'required name=q' } },
+ ...(listed.length ? [{ '@type': 'ItemList', name: 'Deals on OpenVibe.Deals', itemListElement: listed.map((v, i) => ({ '@type': 'ListItem', position: i + 1, url: publication.abs(publication.offerPath(v.root)), name: v.root.title })) }] : []),
home.faqJsonLd(faq),
home.faqJsonLd(faq),
- ],
+ // The publisher of the WebSite above, so its @id resolves on this page too.
+ sharedSeo.jsonLd.organization(),
+ ]),
bodyClass: 'home',
bodyClass: 'home',
body: home.homePage({
body: home.homePage({
empty: total === 0, freshnessHours: Math.round(config.freshnessMs / 3600000), faq,
empty: total === 0, freshnessHours: Math.round(config.freshnessMs / 3600000), faq,
@@ -281,7 +285,7 @@ function createPages(ctx) {
@@ -281,7 +285,7 @@ function createPages(ctx) {
decision: v.decision,
decision: v.decision,
canonical,
canonical,
type: 'website',
type: 'website',
- jsonLd: [publication.offerJsonLd(v), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, ...(v.store ? [{ name: v.store.name || v.store.domain, url: publication.abs(publication.storePath(v.store)) }] : []), { name: r.title, url: canonical }])],
+ jsonLd: oneGraph([publication.offerJsonLd(v), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, ...(v.store ? [{ name: v.store.name || v.store.domain, url: publication.abs(publication.storePath(v.store)) }] : []), { name: r.title, url: canonical }])]),
body: views.offerPage({
body: views.offerPage({
v, dto: publication.offerDto(v), viewer, csrf: person ? csrfToken(config, viewer) : '', myVote: person ? await votes.mine(viewer.subject, r) : 0,
v, dto: publication.offerDto(v), viewer, csrf: person ? csrfToken(config, viewer) : '', myVote: person ? await votes.mine(viewer.subject, r) : 0,
comments: c, mergedComments, canEdit: access.canEdit(viewer, r), isMod,
comments: c, mergedComments, canEdit: access.canEdit(viewer, r), isMod,
@@ -388,7 +392,7 @@ function createPages(ctx) {
@@ -388,7 +392,7 @@ function createPages(ctx) {
title: `${product.name} — price comparison`,
title: `${product.name} — price comparison`,
description: `Offers for ${product.name}, each with the time its price was observed.`,
description: `Offers for ${product.name}, each with the time its price was observed.`,
decision: pv.decision, canonical,
decision: pv.decision, canonical,
- jsonLd: [publication.productJsonLd(pv), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, { name: product.name, url: canonical }])],
+ jsonLd: oneGraph([publication.productJsonLd(pv), seo.structuredData.breadcrumbs([{ name: 'Deals', url: publication.abs('/') }, { name: product.name, url: canonical }])]),
body: views.productPage({ pv, urls }),
body: views.productPage({ pv, urls }),
},
},
};
};
@@ -15,6 +15,15 @@ const path = require('path');
@@ -15,6 +15,15 @@ const path = require('path');
const layout = require('openvibe-publishing/layout');
const layout = require('openvibe-publishing/layout');
const frame = require('openvibe-shared/frame');
const frame = require('openvibe-shared/frame');
+/**
+ * The page's JSON-LD in one script: every node in one @graph, so a page carries exactly one JSON-LD script.
+ * The nodes keep their own fields; only their own @context goes, because the graph carries it once.
+ */
+const oneGraph = (nodes) => {
+ const graph = nodes.filter(Boolean).map((n) => { const c = { ...n }; delete c['@context']; return c; });
+ return [{ '@context': 'https://schema.org', '@graph': graph }];
+};
+
const NETWORK_URL = 'https://openvibe.network';
const NETWORK_URL = 'https://openvibe.network';
const SITE_NAME = 'OpenVibe.Deals';
const SITE_NAME = 'OpenVibe.Deals';
// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.
// One site summary, shared by /llms.txt, /llms-full.txt and the home page's ai-summary.
@@ -113,4 +122,4 @@ function renderPage(o) {
@@ -113,4 +122,4 @@ function renderPage(o) {
});
});
}
}
-module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, SITE_SUMMARY, NETWORK_URL };
+module.exports = { renderPage, asset, assetVersion, setRelease, SITE_NAME, SITE_SUMMARY, NETWORK_URL, oneGraph };
@@ -20,7 +20,7 @@ const { sourceItem } = require('./helpers/mocks');
@@ -20,7 +20,7 @@ const { sourceItem } = require('./helpers/mocks');
assert.match(page.text, /data-freshness="fresh"/);
assert.match(page.text, /data-freshness="fresh"/);
assert.strictEqual(robotsOf(page.text), 'index, follow');
assert.strictEqual(robotsOf(page.text), 'index, follow');
const ld = jsonLd(page.text).find((x) => x['@type'] === 'Product');
const ld = jsonLd(page.text).find((x) => x['@type'] === 'Product');
- assert.strictEqual(ld.offers[0].price, '49.99');
+ assert.strictEqual(ld.offers[0].price, 49.99);
assert.strictEqual(ld.offers[0].priceCurrency, 'USD');
assert.strictEqual(ld.offers[0].priceCurrency, 'USD');
assert.strictEqual(ld.offers[0].availability, 'https://schema.org/InStock');
assert.strictEqual(ld.offers[0].availability, 'https://schema.org/InStock');
const json = await t.offerJson(offer.slug);
const json = await t.offerJson(offer.slug);
@@ -82,7 +82,7 @@ const { sourceItem } = require('./helpers/mocks');
@@ -82,7 +82,7 @@ const { sourceItem } = require('./helpers/mocks');
assert.strictEqual(json.observations[0].observed_by, bob.subject);
assert.strictEqual(json.observations[0].observed_by, bob.subject);
const page = await t.get(`/d/${offer.slug}`);
const page = await t.get(`/d/${offer.slug}`);
assert.strictEqual(robotsOf(page.text), 'index, follow');
assert.strictEqual(robotsOf(page.text), 'index, follow');
- assert.strictEqual(jsonLd(page.text).find((x) => x['@type'] === 'Product').offers[0].price, '44.00');
+ assert.strictEqual(jsonLd(page.text).find((x) => x['@type'] === 'Product').offers[0].price, 44);
});
});
await check('an imported observation is dated by when the source saw it: old retrieval → stale from the start', async () => {
await check('an imported observation is dated by when the source saw it: old retrieval → stale from the start', async () => {
@@ -104,8 +104,15 @@ async function boot(opts = {}) {
@@ -104,8 +104,15 @@ async function boot(opts = {}) {
}
}
/** JSON-LD blocks of an HTML page. */
/** JSON-LD blocks of an HTML page. */
+/** The JSON-LD of a page: every script's nodes, with a one-@graph script flattened into its nodes. */
function jsonLd(htmlText) {
function jsonLd(htmlText) {
- return [...htmlText.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].map((m) => JSON.parse(m[1]));
+ return [...htmlText.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)]
+ .flatMap((m) => { const doc = JSON.parse(m[1]); return Array.isArray(doc['@graph']) ? doc['@graph'] : [doc]; });
+}
+
+/** How many JSON-LD scripts a page carries (the estate ships one per page). */
+function jsonLdScripts(htmlText) {
+ return [...htmlText.matchAll(/<script type="application\/ld\+json">([\s\S]*?)<\/script>/g)].length;
}
}
const robotsOf = (htmlText) => (/<meta name="robots" content="([^"]+)"/.exec(htmlText) || [])[1];
const robotsOf = (htmlText) => (/<meta name="robots" content="([^"]+)"/.exec(htmlText) || [])[1];
@@ -116,4 +123,4 @@ async function check(name, fn) {
@@ -116,4 +123,4 @@ async function check(name, fn) {
}
}
function done() { console.log(failures ? `\n${failures} failed` : '\nall passed'); process.exit(failures ? 1 : 0); }
function done() { console.log(failures ? `\n${failures} failed` : '\nall passed'); process.exit(failures ? 1 : 0); }
-module.exports = { boot, check, done, makeClock, jsonLd, robotsOf, HOUR };
+module.exports = { boot, check, done, makeClock, jsonLd, jsonLdScripts, robotsOf, HOUR };
@@ -60,7 +60,7 @@ const { sourceItem } = require('./helpers/mocks');
@@ -60,7 +60,7 @@ const { sourceItem } = require('./helpers/mocks');
const ld = jsonLd(page.text).find((x) => x['@type'] === 'Product');
const ld = jsonLd(page.text).find((x) => x['@type'] === 'Product');
assert.strictEqual(ld.gtin, '0012345678905');
assert.strictEqual(ld.gtin, '0012345678905');
const prices = ld.offers.map((o) => o.price);
const prices = ld.offers.map((o) => o.price);
- assert.deepStrictEqual(prices, ['30.00', undefined, undefined], 'only the fresh stated price reaches structured data');
+ assert.deepStrictEqual(prices, [30, undefined, undefined], 'only the fresh stated price reaches structured data');
});
});
await check('a price in an imported headline is not parsed into a price', async () => {
await check('a price in an imported headline is not parsed into a price', async () => {
@@ -65,7 +65,7 @@ const { boot, check, done, robotsOf, jsonLd, HOUR } = require('./helpers/boot');
@@ -65,7 +65,7 @@ const { boot, check, done, robotsOf, jsonLd, HOUR } = require('./helpers/boot');
const types = jsonLd(r.text).map((x) => x['@type']);
const types = jsonLd(r.text).map((x) => x['@type']);
assert.deepStrictEqual(types, ['Product', 'BreadcrumbList']);
assert.deepStrictEqual(types, ['Product', 'BreadcrumbList']);
const ld = jsonLd(r.text)[0].offers[0];
const ld = jsonLd(r.text)[0].offers[0];
- assert.deepStrictEqual([ld.price, ld.priceCurrency, ld.itemCondition, ld.seller.name], ['79.00', 'USD', 'https://schema.org/NewCondition', 'Gadgets Inc']);
+ assert.deepStrictEqual([ld.price, ld.priceCurrency, ld.itemCondition, ld.seller.name], [79, 'USD', 'https://schema.org/NewCondition', 'Gadgets Inc']);
assert.strictEqual(ld.shippingDetails.shippingRate.value, '4.99');
assert.strictEqual(ld.shippingDetails.shippingRate.value, '4.99');
assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe.deals\/d\//);
assert.match(r.text, /<link rel="canonical" href="https:\/\/openvibe.deals\/d\//);
const json = await t.get(`/d/${deal.slug}.json`);
const json = await t.get(`/d/${deal.slug}.json`);