brainerce 1.46.2 → 1.47.3

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/dist/index.js CHANGED
@@ -34,6 +34,12 @@ __export(index_exports, {
34
34
  BrainerceError: () => BrainerceError,
35
35
  RTL_LOCALES: () => RTL_LOCALES,
36
36
  SDK_VERSION: () => SDK_VERSION,
37
+ buildArticleJsonLd: () => buildArticleJsonLd,
38
+ buildBreadcrumbJsonLd: () => buildBreadcrumbJsonLd,
39
+ buildCollectionPageJsonLd: () => buildCollectionPageJsonLd,
40
+ buildOrganizationJsonLd: () => buildOrganizationJsonLd,
41
+ buildProductJsonLd: () => buildProductJsonLd,
42
+ buildWebsiteJsonLd: () => buildWebsiteJsonLd,
37
43
  createWebhookHandler: () => createWebhookHandler,
38
44
  deriveSeoDescription: () => deriveSeoDescription,
39
45
  enableDevGuards: () => enableDevGuards,
@@ -41,9 +47,11 @@ __export(index_exports, {
41
47
  formatPrice: () => formatPrice,
42
48
  formatProductPrice: () => formatProductPrice,
43
49
  formatVariantPrice: () => formatVariantPrice,
50
+ getBlogSitemapEntries: () => getBlogSitemapEntries,
44
51
  getCartItemImage: () => getCartItemImage,
45
52
  getCartItemName: () => getCartItemName,
46
53
  getCartTotals: () => getCartTotals,
54
+ getCategorySitemapEntries: () => getCategorySitemapEntries,
47
55
  getDescriptionContent: () => getDescriptionContent,
48
56
  getDirectionForLocale: () => getDirectionForLocale,
49
57
  getPriceDisplay: () => formatPrice,
@@ -61,9 +69,10 @@ __export(index_exports, {
61
69
  isCouponApplicableToProduct: () => isCouponApplicableToProduct,
62
70
  isHtmlDescription: () => isHtmlDescription,
63
71
  isWebhookEventType: () => isWebhookEventType,
72
+ jsonLdScriptProps: () => jsonLdScriptProps,
64
73
  parseWebhookEvent: () => parseWebhookEvent,
65
74
  safePaymentRedirect: () => safePaymentRedirect,
66
- stripHtml: () => stripHtml,
75
+ stripHtml: () => stripHtml2,
67
76
  verifyWebhook: () => verifyWebhook
68
77
  });
69
78
  module.exports = __toCommonJS(index_exports);
@@ -655,6 +664,7 @@ var BrainerceClient = class {
655
664
  this.proxyMode = options.proxyMode || false;
656
665
  this.analyticsBaseUrl = this.resolveAnalyticsBaseUrl(options.analyticsBaseUrl, resolvedBase);
657
666
  this.onAuthError = options.onAuthError;
667
+ this.onCartReset = options.onCartReset;
658
668
  this.hydrateSessionCart();
659
669
  this.detectRecoverCartFromUrl();
660
670
  }
@@ -1314,6 +1324,34 @@ var BrainerceClient = class {
1314
1324
  }
1315
1325
  throw new BrainerceError("getCategories is only available in vibe-coded mode", 400);
1316
1326
  }
1327
+ /**
1328
+ * Get one category by slug — the payload for a storefront category
1329
+ * (collection) landing page: name, description HTML, meta, breadcrumb and
1330
+ * product count. Fetch the products themselves with
1331
+ * `getProducts({ categories: [category.id] })`. Vibe-coded mode only, like
1332
+ * {@link getCategories}.
1333
+ *
1334
+ * @example
1335
+ * ```typescript
1336
+ * const category = await client.getCategoryBySlug('running-shoes').catch(() => null);
1337
+ * if (!category) notFound();
1338
+ * const { data: products } = await client.getProducts({ categories: [category.id] });
1339
+ * ```
1340
+ */
1341
+ async getCategoryBySlug(slug, options) {
1342
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
1343
+ const encodedSlug = encodePathSegment(slug);
1344
+ if (this.isVibeCodedMode()) {
1345
+ return this.vibeCodedRequest(
1346
+ "GET",
1347
+ `/categories/slug/${encodedSlug}`,
1348
+ void 0,
1349
+ void 0,
1350
+ headerOverrides
1351
+ );
1352
+ }
1353
+ throw new BrainerceError("getCategoryBySlug is only available in vibe-coded mode", 400);
1354
+ }
1317
1355
  /**
1318
1356
  * Get available brands for filtering products
1319
1357
  * Works in vibe-coded mode
@@ -4225,14 +4263,17 @@ var BrainerceClient = class {
4225
4263
  const migrated = await this.migrateLocalCartToSession();
4226
4264
  if (migrated) return migrated;
4227
4265
  if (this.sessionCartId && this.sessionToken) {
4266
+ const previousCartId = this.sessionCartId;
4228
4267
  try {
4229
4268
  const cart2 = await this.getCart(this.sessionCartId);
4230
4269
  if (cart2.status === "ACTIVE") {
4231
4270
  return cart2;
4232
4271
  }
4233
4272
  this.clearSessionCart();
4273
+ this.onCartReset?.({ previousCartId, reason: "not_active" });
4234
4274
  } catch {
4235
4275
  this.clearSessionCart();
4276
+ this.onCartReset?.({ previousCartId, reason: "not_found" });
4236
4277
  }
4237
4278
  }
4238
4279
  if (this.sessionToken) {
@@ -6461,20 +6502,23 @@ var BrainerceClient = class {
6461
6502
  );
6462
6503
  }
6463
6504
  // -------------------- Loyalty --------------------
6464
- // Storefront-mode only (storeId + customerToken). The backend exposes loyalty
6465
- // solely under /api/stores/:storeId/loyalty there is no vibe-coded route, so
6466
- // these do NOT branch on isVibeCodedMode() (that would 404 silently).
6505
+ // Available in both storefront mode (storeId + customerToken) and
6506
+ // vibe-coded mode (salesChannelId + customerToken) Phase 5 added the
6507
+ // /api/vc/:connectionId/loyalty/* routes mirroring /api/stores/:storeId/loyalty.
6467
6508
  /**
6468
6509
  * Get the logged-in customer's loyalty status: enrollment, points balance,
6469
- * lifetime earned, and the program's display config (requires customerToken).
6470
- * Only available in storefront mode. `program` is null when the store has no
6471
- * loyalty program.
6510
+ * lifetime earned, the program's display config, earned milestone `badges`,
6511
+ * and the `paidMembership` subscription state (null for free members).
6512
+ * Requires customerToken. Only available in storefront mode. `program` is
6513
+ * null when the store has no loyalty program.
6472
6514
  *
6473
6515
  * @example
6474
6516
  * ```typescript
6475
6517
  * client.setCustomerToken(auth.token);
6476
6518
  * const status = await client.getLoyaltyStatus();
6477
6519
  * if (status.enrolled) console.log(`${status.pointsBalance} ${status.program?.pointsName}`);
6520
+ * status.badges?.forEach((b) => console.log(`🏅 ${b.name}`));
6521
+ * if (status.paidMembership?.status === 'ACTIVE') showPremiumPerks(status.paidMembership.plan);
6478
6522
  * ```
6479
6523
  */
6480
6524
  async getLoyaltyStatus() {
@@ -6484,10 +6528,16 @@ var BrainerceClient = class {
6484
6528
  401
6485
6529
  );
6486
6530
  }
6531
+ if (this.isVibeCodedMode()) {
6532
+ return this.vibeCodedRequest("GET", "/loyalty/me");
6533
+ }
6487
6534
  if (this.storeId && !this.apiKey) {
6488
6535
  return this.storefrontRequest("GET", "/loyalty/me");
6489
6536
  }
6490
- throw new BrainerceError("getLoyaltyStatus is only available in storefront mode", 400);
6537
+ throw new BrainerceError(
6538
+ "getLoyaltyStatus is only available in vibe-coded or storefront mode",
6539
+ 400
6540
+ );
6491
6541
  }
6492
6542
  /**
6493
6543
  * Enroll the logged-in customer in the store's loyalty program (requires
@@ -6506,10 +6556,16 @@ var BrainerceClient = class {
6506
6556
  401
6507
6557
  );
6508
6558
  }
6559
+ if (this.isVibeCodedMode()) {
6560
+ return this.vibeCodedRequest("POST", "/loyalty/enroll");
6561
+ }
6509
6562
  if (this.storeId && !this.apiKey) {
6510
6563
  return this.storefrontRequest("POST", "/loyalty/enroll");
6511
6564
  }
6512
- throw new BrainerceError("enrollInLoyalty is only available in storefront mode", 400);
6565
+ throw new BrainerceError(
6566
+ "enrollInLoyalty is only available in vibe-coded or storefront mode",
6567
+ 400
6568
+ );
6513
6569
  }
6514
6570
  /**
6515
6571
  * List the rewards the customer can redeem points for (active rewards, cheapest
@@ -6528,10 +6584,16 @@ var BrainerceClient = class {
6528
6584
  401
6529
6585
  );
6530
6586
  }
6587
+ if (this.isVibeCodedMode()) {
6588
+ return this.vibeCodedRequest("GET", "/loyalty/rewards/available");
6589
+ }
6531
6590
  if (this.storeId && !this.apiKey) {
6532
6591
  return this.storefrontRequest("GET", "/loyalty/rewards/available");
6533
6592
  }
6534
- throw new BrainerceError("getAvailableRewards is only available in storefront mode", 400);
6593
+ throw new BrainerceError(
6594
+ "getAvailableRewards is only available in vibe-coded or storefront mode",
6595
+ 400
6596
+ );
6535
6597
  }
6536
6598
  /**
6537
6599
  * Redeem a reward: spends the customer's points and mints a one-time coupon
@@ -6551,10 +6613,16 @@ var BrainerceClient = class {
6551
6613
  401
6552
6614
  );
6553
6615
  }
6616
+ if (this.isVibeCodedMode()) {
6617
+ return this.vibeCodedRequest("POST", "/loyalty/redeem", { rewardId });
6618
+ }
6554
6619
  if (this.storeId && !this.apiKey) {
6555
6620
  return this.storefrontRequest("POST", "/loyalty/redeem", { rewardId });
6556
6621
  }
6557
- throw new BrainerceError("redeemLoyaltyReward is only available in storefront mode", 400);
6622
+ throw new BrainerceError(
6623
+ "redeemLoyaltyReward is only available in vibe-coded or storefront mode",
6624
+ 400
6625
+ );
6558
6626
  }
6559
6627
  /**
6560
6628
  * Report a social share, granting the SOCIAL_SHARE earning-rule bonus if the
@@ -6574,6 +6642,13 @@ var BrainerceClient = class {
6574
6642
  401
6575
6643
  );
6576
6644
  }
6645
+ if (this.isVibeCodedMode()) {
6646
+ return this.vibeCodedRequest(
6647
+ "POST",
6648
+ "/loyalty/social-share",
6649
+ platform ? { platform } : {}
6650
+ );
6651
+ }
6577
6652
  if (this.storeId && !this.apiKey) {
6578
6653
  return this.storefrontRequest(
6579
6654
  "POST",
@@ -6581,7 +6656,10 @@ var BrainerceClient = class {
6581
6656
  platform ? { platform } : {}
6582
6657
  );
6583
6658
  }
6584
- throw new BrainerceError("reportSocialShare is only available in storefront mode", 400);
6659
+ throw new BrainerceError(
6660
+ "reportSocialShare is only available in vibe-coded or storefront mode",
6661
+ 400
6662
+ );
6585
6663
  }
6586
6664
  /**
6587
6665
  * Public referral-link lookup: who referred the visitor and what welcome
@@ -6600,12 +6678,231 @@ var BrainerceClient = class {
6600
6678
  * ```
6601
6679
  */
6602
6680
  async getReferralInfo(code) {
6681
+ if (this.isVibeCodedMode()) {
6682
+ return this.vibeCodedRequest("GET", "/loyalty/referral-info", void 0, {
6683
+ code
6684
+ });
6685
+ }
6603
6686
  if (this.storeId && !this.apiKey) {
6604
6687
  return this.storefrontRequest("GET", "/loyalty/referral-info", void 0, {
6605
6688
  code
6606
6689
  });
6607
6690
  }
6608
- throw new BrainerceError("getReferralInfo is only available in storefront mode", 400);
6691
+ throw new BrainerceError(
6692
+ "getReferralInfo is only available in vibe-coded or storefront mode",
6693
+ 400
6694
+ );
6695
+ }
6696
+ /**
6697
+ * AI-recommended reward for the logged-in customer — "recommended for you"
6698
+ * at the top of the rewards list (requires customerToken). The result is
6699
+ * ALWAYS a real reward from the store's catalog (the AI only ranks; a
6700
+ * hallucinated pick falls back to a deterministic choice). Returns
6701
+ * `{ reward: null }` when the catalog is empty. Rate-limited (5/min per
6702
+ * customer — it spends the merchant's AI credits). Only available in
6703
+ * storefront mode.
6704
+ *
6705
+ * @example
6706
+ * ```typescript
6707
+ * const { reward, reason } = await client.getRecommendedReward();
6708
+ * if (reward) showRecommendation(reward, reason);
6709
+ * ```
6710
+ */
6711
+ async getRecommendedReward() {
6712
+ if (!this.customerToken && !this.proxyMode) {
6713
+ throw new BrainerceError(
6714
+ "Customer token required. Call setCustomerToken() after login.",
6715
+ 401
6716
+ );
6717
+ }
6718
+ if (this.isVibeCodedMode()) {
6719
+ return this.vibeCodedRequest(
6720
+ "GET",
6721
+ "/loyalty/rewards/recommended"
6722
+ );
6723
+ }
6724
+ if (this.storeId && !this.apiKey) {
6725
+ return this.storefrontRequest(
6726
+ "GET",
6727
+ "/loyalty/rewards/recommended"
6728
+ );
6729
+ }
6730
+ throw new BrainerceError(
6731
+ "getRecommendedReward is only available in vibe-coded or storefront mode",
6732
+ 400
6733
+ );
6734
+ }
6735
+ /**
6736
+ * List the paid membership plans the customer can subscribe to (requires
6737
+ * customerToken). Empty when the store offers none or the program is not
6738
+ * active. Only available in storefront mode.
6739
+ *
6740
+ * @example
6741
+ * ```typescript
6742
+ * const plans = await client.getMembershipPlans();
6743
+ * ```
6744
+ */
6745
+ async getMembershipPlans() {
6746
+ if (!this.customerToken && !this.proxyMode) {
6747
+ throw new BrainerceError(
6748
+ "Customer token required. Call setCustomerToken() after login.",
6749
+ 401
6750
+ );
6751
+ }
6752
+ if (this.isVibeCodedMode()) {
6753
+ return this.vibeCodedRequest("GET", "/loyalty/membership/plans");
6754
+ }
6755
+ if (this.storeId && !this.apiKey) {
6756
+ return this.storefrontRequest("GET", "/loyalty/membership/plans");
6757
+ }
6758
+ throw new BrainerceError(
6759
+ "getMembershipPlans is only available in vibe-coded or storefront mode",
6760
+ 400
6761
+ );
6762
+ }
6763
+ /**
6764
+ * List the customer's saved payment methods (display fields only — brand /
6765
+ * last4 / expiry, never card data) for the membership subscribe flow.
6766
+ * Requires customerToken. Cards are vaulted by checking out with
6767
+ * `saveCard: true`. Only available in storefront mode.
6768
+ *
6769
+ * @example
6770
+ * ```typescript
6771
+ * const methods = await client.getMySavedPaymentMethods();
6772
+ * ```
6773
+ */
6774
+ async getMySavedPaymentMethods() {
6775
+ if (!this.customerToken && !this.proxyMode) {
6776
+ throw new BrainerceError(
6777
+ "Customer token required. Call setCustomerToken() after login.",
6778
+ 401
6779
+ );
6780
+ }
6781
+ if (this.isVibeCodedMode()) {
6782
+ return this.vibeCodedRequest(
6783
+ "GET",
6784
+ "/loyalty/membership/payment-methods"
6785
+ );
6786
+ }
6787
+ if (this.storeId && !this.apiKey) {
6788
+ return this.storefrontRequest(
6789
+ "GET",
6790
+ "/loyalty/membership/payment-methods"
6791
+ );
6792
+ }
6793
+ throw new BrainerceError(
6794
+ "getMySavedPaymentMethods is only available in vibe-coded or storefront mode",
6795
+ 400
6796
+ );
6797
+ }
6798
+ /**
6799
+ * Subscribe the customer to a paid membership plan — charges the saved card
6800
+ * IMMEDIATELY and starts the recurring cycle (requires customerToken).
6801
+ * Throws a 409 with a `code` ('card_declined' | 'requires_action' | ...)
6802
+ * when the charge fails; 3D-Secure challenges are not supported off-session
6803
+ * and surface as `requires_action`. Only available in storefront mode.
6804
+ *
6805
+ * @example
6806
+ * ```typescript
6807
+ * const membership = await client.subscribeToMembership({
6808
+ * planId: plan.id,
6809
+ * savedPaymentTokenId: method.id,
6810
+ * });
6811
+ * // membership.status === 'ACTIVE'
6812
+ * ```
6813
+ */
6814
+ async subscribeToMembership(params) {
6815
+ if (!this.customerToken && !this.proxyMode) {
6816
+ throw new BrainerceError(
6817
+ "Customer token required. Call setCustomerToken() after login.",
6818
+ 401
6819
+ );
6820
+ }
6821
+ if (this.isVibeCodedMode()) {
6822
+ return this.vibeCodedRequest(
6823
+ "POST",
6824
+ "/loyalty/membership/subscribe",
6825
+ params
6826
+ );
6827
+ }
6828
+ if (this.storeId && !this.apiKey) {
6829
+ return this.storefrontRequest(
6830
+ "POST",
6831
+ "/loyalty/membership/subscribe",
6832
+ params
6833
+ );
6834
+ }
6835
+ throw new BrainerceError(
6836
+ "subscribeToMembership is only available in vibe-coded or storefront mode",
6837
+ 400
6838
+ );
6839
+ }
6840
+ /**
6841
+ * Cancel the customer's paid membership (requires customerToken).
6842
+ * End-of-period semantics: perks continue until `nextBillingAt`, then the
6843
+ * subscription ends without another charge (a PAST_DUE membership cancels
6844
+ * immediately). Re-subscribing to the same plan before period end simply
6845
+ * un-cancels. Only available in storefront mode.
6846
+ *
6847
+ * @example
6848
+ * ```typescript
6849
+ * const membership = await client.cancelMembership();
6850
+ * // membership.cancelAtPeriodEnd === true
6851
+ * ```
6852
+ */
6853
+ async cancelMembership() {
6854
+ if (!this.customerToken && !this.proxyMode) {
6855
+ throw new BrainerceError(
6856
+ "Customer token required. Call setCustomerToken() after login.",
6857
+ 401
6858
+ );
6859
+ }
6860
+ if (this.isVibeCodedMode()) {
6861
+ return this.vibeCodedRequest("POST", "/loyalty/membership/cancel");
6862
+ }
6863
+ if (this.storeId && !this.apiKey) {
6864
+ return this.storefrontRequest("POST", "/loyalty/membership/cancel");
6865
+ }
6866
+ throw new BrainerceError(
6867
+ "cancelMembership is only available in vibe-coded or storefront mode",
6868
+ 400
6869
+ );
6870
+ }
6871
+ /**
6872
+ * Mint a short-lived session for the embeddable loyalty widget (Phase 5) and
6873
+ * return the ready-to-use iframe URL. Requires customerToken. The returned
6874
+ * `embedUrl` is safe to drop straight into an `<iframe src>` — it carries a
6875
+ * scoped ~15-minute session token, never the real customerToken. Re-call this
6876
+ * before the iframe reloads (e.g. on page navigation) to refresh it.
6877
+ *
6878
+ * @example
6879
+ * ```typescript
6880
+ * const { embedUrl } = await client.getLoyaltyWidgetSession();
6881
+ * // <iframe src={embedUrl} width="360" height="420" />
6882
+ * ```
6883
+ */
6884
+ async getLoyaltyWidgetSession() {
6885
+ if (!this.customerToken && !this.proxyMode) {
6886
+ throw new BrainerceError(
6887
+ "Customer token required. Call setCustomerToken() after login.",
6888
+ 401
6889
+ );
6890
+ }
6891
+ let result;
6892
+ if (this.isVibeCodedMode()) {
6893
+ result = await this.vibeCodedRequest("POST", "/loyalty/widget-session");
6894
+ } else if (this.storeId && !this.apiKey) {
6895
+ result = await this.storefrontRequest("POST", "/loyalty/widget-session");
6896
+ } else {
6897
+ throw new BrainerceError(
6898
+ "getLoyaltyWidgetSession is only available in vibe-coded or storefront mode",
6899
+ 400
6900
+ );
6901
+ }
6902
+ return {
6903
+ ...result,
6904
+ embedUrl: `${this.baseUrl}/api/loyalty/embed/${encodePathSegment(result.storeId)}/${encodePathSegment(result.sessionId)}`
6905
+ };
6609
6906
  }
6610
6907
  /**
6611
6908
  * Get the current customer's orders (requires customerToken)
@@ -8892,6 +9189,247 @@ function formatAmount(amount, currency, locale) {
8892
9189
  }
8893
9190
  }
8894
9191
 
9192
+ // src/jsonld.ts
9193
+ function absoluteUrl(siteUrl, path) {
9194
+ if (!path) return void 0;
9195
+ const base = siteUrl.replace(/\/+$/, "");
9196
+ return path.startsWith("http") ? path : `${base}${path.startsWith("/") ? path : `/${path}`}`;
9197
+ }
9198
+ function stripHtml(html) {
9199
+ return (html ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
9200
+ }
9201
+ function buildArticleJsonLd(post, opts) {
9202
+ const url = absoluteUrl(opts.siteUrl, opts.path ?? `/blog/${post.slug}`);
9203
+ const result = {
9204
+ "@context": "https://schema.org",
9205
+ "@type": "Article",
9206
+ headline: post.seoTitle ?? post.title,
9207
+ name: post.title,
9208
+ ...post.excerpt || post.seoDescription ? { description: post.seoDescription ?? post.excerpt } : {},
9209
+ ...url ? { url, mainEntityOfPage: { "@type": "WebPage", "@id": url } } : {},
9210
+ ...post.coverImageUrl ? { image: [post.coverImageUrl] } : {},
9211
+ ...post.publishedAt ? { datePublished: post.publishedAt } : {},
9212
+ dateModified: post.updatedAt,
9213
+ ...post.author ? { author: { "@type": "Person", name: post.author } } : opts.organizationName ? { author: { "@type": "Organization", name: opts.organizationName } } : {},
9214
+ ...opts.organizationName ? { publisher: { "@type": "Organization", name: opts.organizationName } } : {},
9215
+ ...post.tags.length > 0 ? { keywords: post.tags.join(", ") } : {}
9216
+ };
9217
+ return result;
9218
+ }
9219
+ function buildProductJsonLd(product, opts) {
9220
+ const url = absoluteUrl(
9221
+ opts.siteUrl,
9222
+ opts.path ?? (product.slug ? `/products/${product.slug}` : void 0)
9223
+ );
9224
+ const images = (product.images ?? []).map((img) => img.url).filter(Boolean);
9225
+ const brand = opts.brandName ?? product.brands?.[0]?.name;
9226
+ const description = stripHtml(product.description).slice(0, 5e3);
9227
+ const effectivePrice = product.salePrice ?? product.basePrice;
9228
+ const inStock = product.inventory ? (product.inventory.available ?? 0) > 0 : true;
9229
+ const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
9230
+ const itemCondition = "https://schema.org/NewCondition";
9231
+ const shippingDetails = (opts.shipping ?? []).filter((z) => z.amount !== null).map((z) => ({
9232
+ "@type": "OfferShippingDetails",
9233
+ shippingRate: { "@type": "MonetaryAmount", value: z.amount, currency: opts.currency },
9234
+ shippingDestination: { "@type": "DefinedRegion", addressCountry: z.countries },
9235
+ ...z.handlingTime != null || z.minDeliveryDays != null || z.maxDeliveryDays != null ? {
9236
+ deliveryTime: {
9237
+ "@type": "ShippingDeliveryTime",
9238
+ ...z.handlingTime != null ? {
9239
+ handlingTime: {
9240
+ "@type": "QuantitativeValue",
9241
+ minValue: 0,
9242
+ maxValue: z.handlingTime
9243
+ }
9244
+ } : {},
9245
+ ...z.minDeliveryDays != null || z.maxDeliveryDays != null ? {
9246
+ transitTime: {
9247
+ "@type": "QuantitativeValue",
9248
+ minValue: z.minDeliveryDays ?? z.maxDeliveryDays,
9249
+ maxValue: z.maxDeliveryDays ?? z.minDeliveryDays
9250
+ }
9251
+ } : {}
9252
+ }
9253
+ } : {}
9254
+ }));
9255
+ const offer = isVariable ? {
9256
+ "@type": "AggregateOffer",
9257
+ lowPrice: product.priceMin,
9258
+ highPrice: product.priceMax,
9259
+ priceCurrency: opts.currency,
9260
+ availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
9261
+ itemCondition,
9262
+ ...shippingDetails.length > 0 ? { shippingDetails } : {},
9263
+ ...url ? { url } : {}
9264
+ } : {
9265
+ "@type": "Offer",
9266
+ price: effectivePrice,
9267
+ priceCurrency: opts.currency,
9268
+ availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
9269
+ itemCondition,
9270
+ // Only meaningful for an active sale price with a known end date —
9271
+ // a regular (non-sale) price has no expiry to declare.
9272
+ ...product.salePrice && product.salePriceEndsAt ? { priceValidUntil: product.salePriceEndsAt } : {},
9273
+ ...shippingDetails.length > 0 ? { shippingDetails } : {},
9274
+ ...url ? { url } : {}
9275
+ };
9276
+ return {
9277
+ "@context": "https://schema.org",
9278
+ "@type": "Product",
9279
+ name: product.name,
9280
+ ...description ? { description } : {},
9281
+ ...images.length > 0 ? { image: images } : {},
9282
+ ...url ? { url } : {},
9283
+ sku: product.sku,
9284
+ // Identifiers Google uses to match a product to its catalog — key for
9285
+ // free merchant-listing eligibility without a Merchant Center feed.
9286
+ ...product.gtin ? { gtin: product.gtin } : {},
9287
+ ...product.mpn ? { mpn: product.mpn } : {},
9288
+ ...brand ? { brand: { "@type": "Brand", name: brand } } : {},
9289
+ offers: offer,
9290
+ // Google policy: never emit an empty/zero rating block.
9291
+ ...product.reviewCount && product.reviewCount > 0 && product.avgRating ? {
9292
+ aggregateRating: {
9293
+ "@type": "AggregateRating",
9294
+ ratingValue: product.avgRating,
9295
+ reviewCount: product.reviewCount
9296
+ }
9297
+ } : {}
9298
+ };
9299
+ }
9300
+ function buildOrganizationJsonLd(store, opts) {
9301
+ const sameAs = Object.values(store.socialLinks ?? {}).filter(Boolean);
9302
+ return {
9303
+ "@context": "https://schema.org",
9304
+ "@type": "Organization",
9305
+ name: store.name,
9306
+ url: opts.siteUrl,
9307
+ ...store.logo ? { logo: store.logo } : {},
9308
+ ...store.metaDescription ? { description: store.metaDescription } : {},
9309
+ ...store.contactEmail ? { email: store.contactEmail } : {},
9310
+ ...store.contactPhone ? { telephone: store.contactPhone } : {},
9311
+ ...sameAs.length > 0 ? { sameAs } : {}
9312
+ };
9313
+ }
9314
+ function buildWebsiteJsonLd(store, opts) {
9315
+ const base = opts.siteUrl.replace(/\/+$/, "");
9316
+ const result = {
9317
+ "@context": "https://schema.org",
9318
+ "@type": "WebSite",
9319
+ name: store.name,
9320
+ url: opts.siteUrl
9321
+ };
9322
+ if (opts.searchUrlTemplate?.includes("{search_term_string}")) {
9323
+ const tmpl = opts.searchUrlTemplate.startsWith("http") ? opts.searchUrlTemplate : `${base}${opts.searchUrlTemplate.startsWith("/") ? "" : "/"}${opts.searchUrlTemplate}`;
9324
+ result.potentialAction = {
9325
+ "@type": "SearchAction",
9326
+ target: { "@type": "EntryPoint", urlTemplate: tmpl },
9327
+ "query-input": "required name=search_term_string"
9328
+ };
9329
+ }
9330
+ return result;
9331
+ }
9332
+ function buildCollectionPageJsonLd(category, opts) {
9333
+ const url = absoluteUrl(opts.siteUrl, opts.path);
9334
+ const description = category.metaDescription ?? (stripHtml(category.description) || void 0);
9335
+ return {
9336
+ "@context": "https://schema.org",
9337
+ "@type": "CollectionPage",
9338
+ name: category.name,
9339
+ ...description ? { description } : {},
9340
+ ...url ? { url, mainEntityOfPage: { "@type": "WebPage", "@id": url } } : {},
9341
+ ...category.image ? { image: [category.image] } : {}
9342
+ };
9343
+ }
9344
+ function buildBreadcrumbJsonLd(items) {
9345
+ return {
9346
+ "@context": "https://schema.org",
9347
+ "@type": "BreadcrumbList",
9348
+ itemListElement: items.map((item, index) => ({
9349
+ "@type": "ListItem",
9350
+ position: index + 1,
9351
+ name: item.name,
9352
+ item: item.url
9353
+ }))
9354
+ };
9355
+ }
9356
+ function jsonLdScriptProps(data) {
9357
+ return {
9358
+ type: "application/ld+json",
9359
+ dangerouslySetInnerHTML: { __html: JSON.stringify(data).replace(/</g, "\\u003c") }
9360
+ };
9361
+ }
9362
+
9363
+ // src/sitemap.ts
9364
+ async function getBlogSitemapEntries(client, opts) {
9365
+ const base = opts.siteUrl.replace(/\/+$/, "");
9366
+ const basePath = opts.basePath ?? "/blog";
9367
+ const pageSize = Math.min(opts.pageSize ?? 100, 100);
9368
+ const maxEntries = opts.maxEntries ?? 5e3;
9369
+ const posts = [];
9370
+ let page = 1;
9371
+ for (; ; ) {
9372
+ const res = await client.blog.getPosts({ page, limit: pageSize });
9373
+ posts.push(...res.data.map((p) => ({ slug: p.slug, updatedAt: p.updatedAt })));
9374
+ if (page >= res.meta.totalPages || posts.length >= maxEntries) break;
9375
+ page += 1;
9376
+ }
9377
+ const nonDefaultLocales = opts.locales?.filter((locale) => locale !== opts.defaultLocale) ?? [];
9378
+ const entries = [];
9379
+ entries.push({ url: `${base}${basePath}`, changeFrequency: "daily", priority: 0.7 });
9380
+ for (const locale of nonDefaultLocales) {
9381
+ entries.push({
9382
+ url: `${base}/${locale}${basePath}`,
9383
+ changeFrequency: "daily",
9384
+ priority: 0.7
9385
+ });
9386
+ }
9387
+ for (const post of posts.slice(0, maxEntries)) {
9388
+ const lastModified = new Date(post.updatedAt);
9389
+ entries.push({
9390
+ url: `${base}${basePath}/${post.slug}`,
9391
+ lastModified,
9392
+ changeFrequency: "weekly",
9393
+ priority: 0.6
9394
+ });
9395
+ for (const locale of nonDefaultLocales) {
9396
+ entries.push({
9397
+ url: `${base}/${locale}${basePath}/${post.slug}`,
9398
+ lastModified,
9399
+ changeFrequency: "weekly",
9400
+ priority: 0.5
9401
+ });
9402
+ }
9403
+ }
9404
+ return entries;
9405
+ }
9406
+ async function getCategorySitemapEntries(client, opts) {
9407
+ const base = opts.siteUrl.replace(/\/+$/, "");
9408
+ const basePath = opts.basePath ?? "/category";
9409
+ const nonDefaultLocales = opts.locales?.filter((l) => l !== opts.defaultLocale) ?? [];
9410
+ const { categories } = await client.getCategories();
9411
+ const slugs = [];
9412
+ const walk = (nodes) => {
9413
+ for (const node of nodes) {
9414
+ if (node.slug) slugs.push(node.slug);
9415
+ if (node.children?.length) walk(node.children);
9416
+ }
9417
+ };
9418
+ walk(categories);
9419
+ const entries = [];
9420
+ for (const slug of slugs) {
9421
+ entries.push({ url: `${base}${basePath}/${slug}`, changeFrequency: "daily", priority: 0.8 });
9422
+ for (const locale of nonDefaultLocales) {
9423
+ entries.push({
9424
+ url: `${base}/${locale}${basePath}/${slug}`,
9425
+ changeFrequency: "daily",
9426
+ priority: 0.7
9427
+ });
9428
+ }
9429
+ }
9430
+ return entries;
9431
+ }
9432
+
8895
9433
  // src/types.ts
8896
9434
  function isHtmlDescription(product) {
8897
9435
  if (product?.descriptionFormat === "html") return true;
@@ -8907,7 +9445,7 @@ function getDescriptionContent(product) {
8907
9445
  }
8908
9446
  return { text: product.description };
8909
9447
  }
8910
- function stripHtml(html) {
9448
+ function stripHtml2(html) {
8911
9449
  if (!html) return "";
8912
9450
  return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&nbsp;/gi, " ").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;/gi, "'").replace(/&#x27;/gi, "'").replace(/&#x2F;/gi, "/").replace(/\s+/g, " ").trim();
8913
9451
  }
@@ -8916,7 +9454,7 @@ function deriveSeoDescription(product, options) {
8916
9454
  const maxLength = options?.maxLength ?? 160;
8917
9455
  const authored = (product.seoDescription ?? product.metaDescription ?? "").trim();
8918
9456
  if (authored) return authored;
8919
- const plain = stripHtml(product.description);
9457
+ const plain = stripHtml2(product.description);
8920
9458
  if (plain) {
8921
9459
  if (plain.length <= maxLength) return plain;
8922
9460
  const sliceLength = maxLength - 1;
@@ -9138,6 +9676,12 @@ function isCouponApplicableToProduct(coupon, productId) {
9138
9676
  BrainerceError,
9139
9677
  RTL_LOCALES,
9140
9678
  SDK_VERSION,
9679
+ buildArticleJsonLd,
9680
+ buildBreadcrumbJsonLd,
9681
+ buildCollectionPageJsonLd,
9682
+ buildOrganizationJsonLd,
9683
+ buildProductJsonLd,
9684
+ buildWebsiteJsonLd,
9141
9685
  createWebhookHandler,
9142
9686
  deriveSeoDescription,
9143
9687
  enableDevGuards,
@@ -9145,9 +9689,11 @@ function isCouponApplicableToProduct(coupon, productId) {
9145
9689
  formatPrice,
9146
9690
  formatProductPrice,
9147
9691
  formatVariantPrice,
9692
+ getBlogSitemapEntries,
9148
9693
  getCartItemImage,
9149
9694
  getCartItemName,
9150
9695
  getCartTotals,
9696
+ getCategorySitemapEntries,
9151
9697
  getDescriptionContent,
9152
9698
  getDirectionForLocale,
9153
9699
  getPriceDisplay,
@@ -9165,6 +9711,7 @@ function isCouponApplicableToProduct(coupon, productId) {
9165
9711
  isCouponApplicableToProduct,
9166
9712
  isHtmlDescription,
9167
9713
  isWebhookEventType,
9714
+ jsonLdScriptProps,
9168
9715
  parseWebhookEvent,
9169
9716
  safePaymentRedirect,
9170
9717
  stripHtml,