@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.38",
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
  */