@thg-altitude/schemaorg 1.0.38 → 1.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thg-altitude/schemaorg",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.42",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -11,6 +11,6 @@
|
|
|
11
11
|
"author": "Phillip Gourley",
|
|
12
12
|
"license": "ISC",
|
|
13
13
|
"peerDependencies": {
|
|
14
|
-
"astro": "^4.0.8 || ^5.0.0"
|
|
14
|
+
"astro": "^4.0.8 || ^5.0.0 || ^6.0.0"
|
|
15
15
|
}
|
|
16
16
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { mapProductSchemaDataEnhanced } from '../utils/productSchema.js';
|
|
2
|
+
import { mapProductSchemaDataEnhanced, buildSubscriptionFromContracts } from '../utils/productSchema.js';
|
|
3
3
|
|
|
4
4
|
const {
|
|
5
5
|
image,
|
|
@@ -30,6 +30,9 @@ const {
|
|
|
30
30
|
beauty_targetAge,
|
|
31
31
|
gender,
|
|
32
32
|
weight,
|
|
33
|
+
subscription, // Optional: explicit subscription pricing data { price, priceCurrency, billingIncrement, unitCode, billingDuration }
|
|
34
|
+
subscriptionConfig, // Optional: config for auto-detecting from subscriptionContracts on variants
|
|
35
|
+
// { discountPercentage: 21, frequencyUnit: "MONTH", frequencyValue: 1 }
|
|
33
36
|
colorVariantPages, // Array of {url, sku, color} for sibling color pages
|
|
34
37
|
currentColor, // The color of the current page (used to filter variants)
|
|
35
38
|
baseProductGroupID, // Optional: explicit base product group ID without color suffix
|
|
@@ -354,6 +357,7 @@ function generateProductSchema() {
|
|
|
354
357
|
...(variant?.barcode && { gtin13: variant.barcode.toString() }),
|
|
355
358
|
offers: {
|
|
356
359
|
"@type": "Offer",
|
|
360
|
+
sku: variant.sku.toString(),
|
|
357
361
|
url: `${pageUrl}?variation=${variant?.sku}`,
|
|
358
362
|
price: parseFloat(variant.price.price.amount),
|
|
359
363
|
priceCurrency: currency,
|
|
@@ -385,6 +389,26 @@ function generateProductSchema() {
|
|
|
385
389
|
},
|
|
386
390
|
]
|
|
387
391
|
: []),
|
|
392
|
+
...(() => {
|
|
393
|
+
// Subscription pricing: subscriptionContracts + config, or explicit subscription prop
|
|
394
|
+
const variantPrice = parseFloat(variant.price.price.amount);
|
|
395
|
+
let effectiveSub;
|
|
396
|
+
if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
|
|
397
|
+
effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, currency);
|
|
398
|
+
} else if (subscription) {
|
|
399
|
+
effectiveSub = { ...subscription };
|
|
400
|
+
}
|
|
401
|
+
if (!effectiveSub) return [];
|
|
402
|
+
return [{
|
|
403
|
+
"@type": "UnitPriceSpecification",
|
|
404
|
+
priceComponentType: "https://schema.org/Subscription",
|
|
405
|
+
...(effectiveSub.price != null && { price: effectiveSub.price }),
|
|
406
|
+
...(effectiveSub.priceCurrency && { priceCurrency: effectiveSub.priceCurrency }),
|
|
407
|
+
...(effectiveSub.billingIncrement != null && { billingIncrement: effectiveSub.billingIncrement }),
|
|
408
|
+
...(effectiveSub.unitCode && { unitCode: effectiveSub.unitCode }),
|
|
409
|
+
...(effectiveSub.billingDuration != null && { billingDuration: effectiveSub.billingDuration }),
|
|
410
|
+
}];
|
|
411
|
+
})(),
|
|
388
412
|
],
|
|
389
413
|
}
|
|
390
414
|
};
|
|
@@ -488,6 +512,26 @@ function generateProductSchema() {
|
|
|
488
512
|
},
|
|
489
513
|
]
|
|
490
514
|
: []),
|
|
515
|
+
...(() => {
|
|
516
|
+
// Subscription pricing: subscriptionContracts + config, or explicit subscription prop
|
|
517
|
+
const variantPrice = parseFloat(variant.price.price.amount);
|
|
518
|
+
let effectiveSub;
|
|
519
|
+
if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
|
|
520
|
+
effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, currency);
|
|
521
|
+
} else if (subscription) {
|
|
522
|
+
effectiveSub = { ...subscription };
|
|
523
|
+
}
|
|
524
|
+
if (!effectiveSub) return [];
|
|
525
|
+
return [{
|
|
526
|
+
"@type": "UnitPriceSpecification",
|
|
527
|
+
priceComponentType: "https://schema.org/Subscription",
|
|
528
|
+
...(effectiveSub.price != null && { price: effectiveSub.price }),
|
|
529
|
+
...(effectiveSub.priceCurrency && { priceCurrency: effectiveSub.priceCurrency }),
|
|
530
|
+
...(effectiveSub.billingIncrement != null && { billingIncrement: effectiveSub.billingIncrement }),
|
|
531
|
+
...(effectiveSub.unitCode && { unitCode: effectiveSub.unitCode }),
|
|
532
|
+
...(effectiveSub.billingDuration != null && { billingDuration: effectiveSub.billingDuration }),
|
|
533
|
+
}];
|
|
534
|
+
})(),
|
|
491
535
|
],
|
|
492
536
|
};
|
|
493
537
|
});
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
---
|
|
2
|
-
import { mapProductSchemaData, stripHtml } from '../utils/productSchema.js';
|
|
2
|
+
import { mapProductSchemaData, buildSubscriptionFromContracts, stripHtml } from '../utils/productSchema.js';
|
|
3
3
|
|
|
4
4
|
// Use the mapping function to extract data from the full product data
|
|
5
5
|
const mappedData = mapProductSchemaData(Astro.props);
|
|
6
6
|
|
|
7
|
+
// Subscription data resolution:
|
|
8
|
+
// 1. Explicit subscription prop: { price, priceCurrency, billingIncrement, unitCode, billingDuration }
|
|
9
|
+
// 2. Auto-detect from variant.subscriptionContracts + subscriptionConfig prop
|
|
10
|
+
// subscriptionConfig: { discountPercentage, frequencyUnit, frequencyValue, billingDuration }
|
|
11
|
+
const subscription = Astro.props.subscription || null;
|
|
12
|
+
const subscriptionConfig = Astro.props.subscriptionConfig;
|
|
13
|
+
|
|
7
14
|
// Extract suggestedGender and suggestedAge using the comprehensive mapping function
|
|
8
15
|
let suggestedGender = Astro.props.suggestedGender || mappedData.suggestedGender;
|
|
9
16
|
let suggestedAge = Astro.props.suggestedAge || mappedData.suggestedAge;
|
|
@@ -245,6 +252,7 @@ if (hasMultipleVariants) {
|
|
|
245
252
|
|
|
246
253
|
const offerObj = {
|
|
247
254
|
"@type": "Offer",
|
|
255
|
+
"sku": variant.sku.toString(),
|
|
248
256
|
"url": `${url}${Astro.props.url || ""}?variation=${variant.sku}`,
|
|
249
257
|
"itemCondition": "https://schema.org/NewCondition",
|
|
250
258
|
"availability": variant.inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock"
|
|
@@ -252,6 +260,39 @@ if (hasMultipleVariants) {
|
|
|
252
260
|
|
|
253
261
|
if (variant?.price?.price?.amount) offerObj.price = parseFloat(variant.price.price.amount);
|
|
254
262
|
if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
|
|
263
|
+
|
|
264
|
+
// Add subscription pricing:
|
|
265
|
+
// 1. subscriptionContracts + subscriptionConfig (subscribe & gain / auto-replenish)
|
|
266
|
+
// 2. Explicit subscription prop
|
|
267
|
+
// 3. Box subscription variant (isSubscription + subscriptionTerm/subscriptionFrequency)
|
|
268
|
+
const variantPrice = variant?.price?.price?.amount ? parseFloat(variant.price.price.amount) : null;
|
|
269
|
+
let effectiveSub;
|
|
270
|
+
if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
|
|
271
|
+
effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, Astro.props.currency);
|
|
272
|
+
} else if (variant.isSubscription) {
|
|
273
|
+
// Box subscription variant - build from variant's own subscription fields
|
|
274
|
+
effectiveSub = {
|
|
275
|
+
price: variantPrice,
|
|
276
|
+
priceCurrency: Astro.props.currency || null,
|
|
277
|
+
billingIncrement: variant.subscriptionFrequency ?? 1,
|
|
278
|
+
unitCode: 'MON',
|
|
279
|
+
billingDuration: variant.subscriptionTerm ?? null,
|
|
280
|
+
};
|
|
281
|
+
} else if (subscription) {
|
|
282
|
+
effectiveSub = { ...subscription };
|
|
283
|
+
}
|
|
284
|
+
if (effectiveSub) {
|
|
285
|
+
const subscriptionSpec = {
|
|
286
|
+
"@type": "UnitPriceSpecification",
|
|
287
|
+
"priceComponentType": "https://schema.org/Subscription"
|
|
288
|
+
};
|
|
289
|
+
if (effectiveSub.price != null) subscriptionSpec.price = effectiveSub.price;
|
|
290
|
+
if (effectiveSub.priceCurrency) subscriptionSpec.priceCurrency = effectiveSub.priceCurrency;
|
|
291
|
+
if (effectiveSub.billingIncrement != null) subscriptionSpec.billingIncrement = effectiveSub.billingIncrement;
|
|
292
|
+
if (effectiveSub.unitCode) subscriptionSpec.unitCode = effectiveSub.unitCode;
|
|
293
|
+
if (effectiveSub.billingDuration != null) subscriptionSpec.billingDuration = effectiveSub.billingDuration;
|
|
294
|
+
offerObj.priceSpecification = subscriptionSpec;
|
|
295
|
+
}
|
|
255
296
|
|
|
256
297
|
variantObj.offers = offerObj;
|
|
257
298
|
variantsArray.push(variantObj);
|
|
@@ -358,6 +399,39 @@ if (hasMultipleVariants) {
|
|
|
358
399
|
|
|
359
400
|
if (variant?.price?.price?.amount) offerObj.price = parseFloat(variant.price.price.amount);
|
|
360
401
|
if (Astro.props.currency) offerObj.priceCurrency = Astro.props.currency;
|
|
402
|
+
|
|
403
|
+
// Add subscription pricing:
|
|
404
|
+
// 1. subscriptionContracts + subscriptionConfig (subscribe & gain / auto-replenish)
|
|
405
|
+
// 2. Box subscription variant (isSubscription + subscriptionTerm/subscriptionFrequency)
|
|
406
|
+
// 3. Explicit subscription prop
|
|
407
|
+
const variantPrice = variant?.price?.price?.amount ? parseFloat(variant.price.price.amount) : null;
|
|
408
|
+
let effectiveSub;
|
|
409
|
+
if (variant.subscriptionContracts?.length > 0 && subscriptionConfig) {
|
|
410
|
+
effectiveSub = buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, Astro.props.currency);
|
|
411
|
+
} else if (variant.isSubscription) {
|
|
412
|
+
// Box subscription variant - build from variant's own subscription fields
|
|
413
|
+
effectiveSub = {
|
|
414
|
+
price: variantPrice,
|
|
415
|
+
priceCurrency: Astro.props.currency || null,
|
|
416
|
+
billingIncrement: variant.subscriptionFrequency ?? 1,
|
|
417
|
+
unitCode: 'MON',
|
|
418
|
+
billingDuration: variant.subscriptionTerm ?? null,
|
|
419
|
+
};
|
|
420
|
+
} else if (subscription) {
|
|
421
|
+
effectiveSub = { ...subscription };
|
|
422
|
+
}
|
|
423
|
+
if (effectiveSub) {
|
|
424
|
+
const subscriptionSpec = {
|
|
425
|
+
"@type": "UnitPriceSpecification",
|
|
426
|
+
"priceComponentType": "https://schema.org/Subscription"
|
|
427
|
+
};
|
|
428
|
+
if (effectiveSub.price != null) subscriptionSpec.price = effectiveSub.price;
|
|
429
|
+
if (effectiveSub.priceCurrency) subscriptionSpec.priceCurrency = effectiveSub.priceCurrency;
|
|
430
|
+
if (effectiveSub.billingIncrement != null) subscriptionSpec.billingIncrement = effectiveSub.billingIncrement;
|
|
431
|
+
if (effectiveSub.unitCode) subscriptionSpec.unitCode = effectiveSub.unitCode;
|
|
432
|
+
if (effectiveSub.billingDuration != null) subscriptionSpec.billingDuration = effectiveSub.billingDuration;
|
|
433
|
+
offerObj.priceSpecification = subscriptionSpec;
|
|
434
|
+
}
|
|
361
435
|
|
|
362
436
|
offersArray.push(offerObj);
|
|
363
437
|
}
|
|
@@ -462,4 +536,4 @@ if (reviewsArray.length > 0) schema.review = reviewsArray;
|
|
|
462
536
|
return undefined;
|
|
463
537
|
}
|
|
464
538
|
return value;
|
|
465
|
-
})}></script>
|
|
539
|
+
})}></script>
|
|
@@ -87,6 +87,66 @@ export function mapProductSchemaDataEnhanced(productData) {
|
|
|
87
87
|
return mapProductSchemaData(productData);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Checks if a variant has subscription contracts and builds subscription data
|
|
92
|
+
* from the variant's subscriptionContracts array combined with a subscriptionConfig.
|
|
93
|
+
*
|
|
94
|
+
* The subscriptionConfig provides the discount/frequency details that aren't
|
|
95
|
+
* available on the contract objects themselves.
|
|
96
|
+
*
|
|
97
|
+
* @param {Object} variant - The variant object with subscriptionContracts and price data
|
|
98
|
+
* @param {Object} subscriptionConfig - Config with discount/frequency defaults:
|
|
99
|
+
* {
|
|
100
|
+
* discountPercentage: 21, // % discount for subscription (applied to RRP when available)
|
|
101
|
+
* frequencyUnit: "MONTH", // MONTH, WEEK, DAY, YEAR
|
|
102
|
+
* frequencyValue: 1, // billing increment (1 = every month)
|
|
103
|
+
* billingDuration: null, // null = ongoing
|
|
104
|
+
* }
|
|
105
|
+
* @param {number} variantPrice - The variant's current/sale price (fallback if no RRP)
|
|
106
|
+
* @param {string} currency - Currency code (e.g. "GBP")
|
|
107
|
+
* @returns {Object|null} Mapped subscription data or null
|
|
108
|
+
*/
|
|
109
|
+
export function buildSubscriptionFromContracts(variant, subscriptionConfig, variantPrice, currency) {
|
|
110
|
+
if (!variant?.subscriptionContracts || variant?.subscriptionContracts?.length === 0) return null;
|
|
111
|
+
if (!subscriptionConfig) return null;
|
|
112
|
+
|
|
113
|
+
const discountPct = subscriptionConfig.discountPercentage ?? 0;
|
|
114
|
+
|
|
115
|
+
// Use RRP as the base price for discount calculation when available
|
|
116
|
+
// The subscription discount is typically applied to the RRP (full price), not the sale price
|
|
117
|
+
const rrpPrice = variant?.price?.rrp?.amount ? parseFloat(variant.price.rrp.amount) : null;
|
|
118
|
+
const basePrice = rrpPrice ?? variantPrice;
|
|
119
|
+
|
|
120
|
+
// Compute discounted price from the base price (RRP or sale price)
|
|
121
|
+
let subscriptionPrice = null;
|
|
122
|
+
if (basePrice != null && discountPct > 0) {
|
|
123
|
+
subscriptionPrice = parseFloat((basePrice * (1 - discountPct / 100)).toFixed(2));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Map frequency unit
|
|
127
|
+
const frequencyUnit = (subscriptionConfig.frequencyUnit || 'MONTH').toUpperCase();
|
|
128
|
+
const unitCodeMap = {
|
|
129
|
+
'MONTH': 'MON',
|
|
130
|
+
'WEEK': 'WEE',
|
|
131
|
+
'DAY': 'DAY',
|
|
132
|
+
'YEAR': 'ANN',
|
|
133
|
+
};
|
|
134
|
+
const unitCode = unitCodeMap[frequencyUnit] || frequencyUnit;
|
|
135
|
+
|
|
136
|
+
const billingIncrement = subscriptionConfig.frequencyValue ?? 1;
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
price: subscriptionPrice,
|
|
140
|
+
priceCurrency: currency || null,
|
|
141
|
+
billingIncrement,
|
|
142
|
+
unitCode,
|
|
143
|
+
billingDuration: subscriptionConfig.billingDuration ?? null,
|
|
144
|
+
initialDiscountPercentage: discountPct,
|
|
145
|
+
recurringDiscountPercentage: subscriptionConfig.recurringDiscountPercentage ?? discountPct,
|
|
146
|
+
contractIds: variant.subscriptionContracts.map(c => c.id),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
90
150
|
/**
|
|
91
151
|
* Simple function to strip HTML tags from text
|
|
92
152
|
*/
|