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.mjs CHANGED
@@ -585,6 +585,7 @@ var BrainerceClient = class {
585
585
  this.proxyMode = options.proxyMode || false;
586
586
  this.analyticsBaseUrl = this.resolveAnalyticsBaseUrl(options.analyticsBaseUrl, resolvedBase);
587
587
  this.onAuthError = options.onAuthError;
588
+ this.onCartReset = options.onCartReset;
588
589
  this.hydrateSessionCart();
589
590
  this.detectRecoverCartFromUrl();
590
591
  }
@@ -1244,6 +1245,34 @@ var BrainerceClient = class {
1244
1245
  }
1245
1246
  throw new BrainerceError("getCategories is only available in vibe-coded mode", 400);
1246
1247
  }
1248
+ /**
1249
+ * Get one category by slug — the payload for a storefront category
1250
+ * (collection) landing page: name, description HTML, meta, breadcrumb and
1251
+ * product count. Fetch the products themselves with
1252
+ * `getProducts({ categories: [category.id] })`. Vibe-coded mode only, like
1253
+ * {@link getCategories}.
1254
+ *
1255
+ * @example
1256
+ * ```typescript
1257
+ * const category = await client.getCategoryBySlug('running-shoes').catch(() => null);
1258
+ * if (!category) notFound();
1259
+ * const { data: products } = await client.getProducts({ categories: [category.id] });
1260
+ * ```
1261
+ */
1262
+ async getCategoryBySlug(slug, options) {
1263
+ const headerOverrides = options?.locale ? { "Accept-Language": options.locale } : void 0;
1264
+ const encodedSlug = encodePathSegment(slug);
1265
+ if (this.isVibeCodedMode()) {
1266
+ return this.vibeCodedRequest(
1267
+ "GET",
1268
+ `/categories/slug/${encodedSlug}`,
1269
+ void 0,
1270
+ void 0,
1271
+ headerOverrides
1272
+ );
1273
+ }
1274
+ throw new BrainerceError("getCategoryBySlug is only available in vibe-coded mode", 400);
1275
+ }
1247
1276
  /**
1248
1277
  * Get available brands for filtering products
1249
1278
  * Works in vibe-coded mode
@@ -4155,14 +4184,17 @@ var BrainerceClient = class {
4155
4184
  const migrated = await this.migrateLocalCartToSession();
4156
4185
  if (migrated) return migrated;
4157
4186
  if (this.sessionCartId && this.sessionToken) {
4187
+ const previousCartId = this.sessionCartId;
4158
4188
  try {
4159
4189
  const cart2 = await this.getCart(this.sessionCartId);
4160
4190
  if (cart2.status === "ACTIVE") {
4161
4191
  return cart2;
4162
4192
  }
4163
4193
  this.clearSessionCart();
4194
+ this.onCartReset?.({ previousCartId, reason: "not_active" });
4164
4195
  } catch {
4165
4196
  this.clearSessionCart();
4197
+ this.onCartReset?.({ previousCartId, reason: "not_found" });
4166
4198
  }
4167
4199
  }
4168
4200
  if (this.sessionToken) {
@@ -6391,20 +6423,23 @@ var BrainerceClient = class {
6391
6423
  );
6392
6424
  }
6393
6425
  // -------------------- Loyalty --------------------
6394
- // Storefront-mode only (storeId + customerToken). The backend exposes loyalty
6395
- // solely under /api/stores/:storeId/loyalty there is no vibe-coded route, so
6396
- // these do NOT branch on isVibeCodedMode() (that would 404 silently).
6426
+ // Available in both storefront mode (storeId + customerToken) and
6427
+ // vibe-coded mode (salesChannelId + customerToken) Phase 5 added the
6428
+ // /api/vc/:connectionId/loyalty/* routes mirroring /api/stores/:storeId/loyalty.
6397
6429
  /**
6398
6430
  * Get the logged-in customer's loyalty status: enrollment, points balance,
6399
- * lifetime earned, and the program's display config (requires customerToken).
6400
- * Only available in storefront mode. `program` is null when the store has no
6401
- * loyalty program.
6431
+ * lifetime earned, the program's display config, earned milestone `badges`,
6432
+ * and the `paidMembership` subscription state (null for free members).
6433
+ * Requires customerToken. Only available in storefront mode. `program` is
6434
+ * null when the store has no loyalty program.
6402
6435
  *
6403
6436
  * @example
6404
6437
  * ```typescript
6405
6438
  * client.setCustomerToken(auth.token);
6406
6439
  * const status = await client.getLoyaltyStatus();
6407
6440
  * if (status.enrolled) console.log(`${status.pointsBalance} ${status.program?.pointsName}`);
6441
+ * status.badges?.forEach((b) => console.log(`🏅 ${b.name}`));
6442
+ * if (status.paidMembership?.status === 'ACTIVE') showPremiumPerks(status.paidMembership.plan);
6408
6443
  * ```
6409
6444
  */
6410
6445
  async getLoyaltyStatus() {
@@ -6414,10 +6449,16 @@ var BrainerceClient = class {
6414
6449
  401
6415
6450
  );
6416
6451
  }
6452
+ if (this.isVibeCodedMode()) {
6453
+ return this.vibeCodedRequest("GET", "/loyalty/me");
6454
+ }
6417
6455
  if (this.storeId && !this.apiKey) {
6418
6456
  return this.storefrontRequest("GET", "/loyalty/me");
6419
6457
  }
6420
- throw new BrainerceError("getLoyaltyStatus is only available in storefront mode", 400);
6458
+ throw new BrainerceError(
6459
+ "getLoyaltyStatus is only available in vibe-coded or storefront mode",
6460
+ 400
6461
+ );
6421
6462
  }
6422
6463
  /**
6423
6464
  * Enroll the logged-in customer in the store's loyalty program (requires
@@ -6436,10 +6477,16 @@ var BrainerceClient = class {
6436
6477
  401
6437
6478
  );
6438
6479
  }
6480
+ if (this.isVibeCodedMode()) {
6481
+ return this.vibeCodedRequest("POST", "/loyalty/enroll");
6482
+ }
6439
6483
  if (this.storeId && !this.apiKey) {
6440
6484
  return this.storefrontRequest("POST", "/loyalty/enroll");
6441
6485
  }
6442
- throw new BrainerceError("enrollInLoyalty is only available in storefront mode", 400);
6486
+ throw new BrainerceError(
6487
+ "enrollInLoyalty is only available in vibe-coded or storefront mode",
6488
+ 400
6489
+ );
6443
6490
  }
6444
6491
  /**
6445
6492
  * List the rewards the customer can redeem points for (active rewards, cheapest
@@ -6458,10 +6505,16 @@ var BrainerceClient = class {
6458
6505
  401
6459
6506
  );
6460
6507
  }
6508
+ if (this.isVibeCodedMode()) {
6509
+ return this.vibeCodedRequest("GET", "/loyalty/rewards/available");
6510
+ }
6461
6511
  if (this.storeId && !this.apiKey) {
6462
6512
  return this.storefrontRequest("GET", "/loyalty/rewards/available");
6463
6513
  }
6464
- throw new BrainerceError("getAvailableRewards is only available in storefront mode", 400);
6514
+ throw new BrainerceError(
6515
+ "getAvailableRewards is only available in vibe-coded or storefront mode",
6516
+ 400
6517
+ );
6465
6518
  }
6466
6519
  /**
6467
6520
  * Redeem a reward: spends the customer's points and mints a one-time coupon
@@ -6481,10 +6534,16 @@ var BrainerceClient = class {
6481
6534
  401
6482
6535
  );
6483
6536
  }
6537
+ if (this.isVibeCodedMode()) {
6538
+ return this.vibeCodedRequest("POST", "/loyalty/redeem", { rewardId });
6539
+ }
6484
6540
  if (this.storeId && !this.apiKey) {
6485
6541
  return this.storefrontRequest("POST", "/loyalty/redeem", { rewardId });
6486
6542
  }
6487
- throw new BrainerceError("redeemLoyaltyReward is only available in storefront mode", 400);
6543
+ throw new BrainerceError(
6544
+ "redeemLoyaltyReward is only available in vibe-coded or storefront mode",
6545
+ 400
6546
+ );
6488
6547
  }
6489
6548
  /**
6490
6549
  * Report a social share, granting the SOCIAL_SHARE earning-rule bonus if the
@@ -6504,6 +6563,13 @@ var BrainerceClient = class {
6504
6563
  401
6505
6564
  );
6506
6565
  }
6566
+ if (this.isVibeCodedMode()) {
6567
+ return this.vibeCodedRequest(
6568
+ "POST",
6569
+ "/loyalty/social-share",
6570
+ platform ? { platform } : {}
6571
+ );
6572
+ }
6507
6573
  if (this.storeId && !this.apiKey) {
6508
6574
  return this.storefrontRequest(
6509
6575
  "POST",
@@ -6511,7 +6577,10 @@ var BrainerceClient = class {
6511
6577
  platform ? { platform } : {}
6512
6578
  );
6513
6579
  }
6514
- throw new BrainerceError("reportSocialShare is only available in storefront mode", 400);
6580
+ throw new BrainerceError(
6581
+ "reportSocialShare is only available in vibe-coded or storefront mode",
6582
+ 400
6583
+ );
6515
6584
  }
6516
6585
  /**
6517
6586
  * Public referral-link lookup: who referred the visitor and what welcome
@@ -6530,12 +6599,231 @@ var BrainerceClient = class {
6530
6599
  * ```
6531
6600
  */
6532
6601
  async getReferralInfo(code) {
6602
+ if (this.isVibeCodedMode()) {
6603
+ return this.vibeCodedRequest("GET", "/loyalty/referral-info", void 0, {
6604
+ code
6605
+ });
6606
+ }
6533
6607
  if (this.storeId && !this.apiKey) {
6534
6608
  return this.storefrontRequest("GET", "/loyalty/referral-info", void 0, {
6535
6609
  code
6536
6610
  });
6537
6611
  }
6538
- throw new BrainerceError("getReferralInfo is only available in storefront mode", 400);
6612
+ throw new BrainerceError(
6613
+ "getReferralInfo is only available in vibe-coded or storefront mode",
6614
+ 400
6615
+ );
6616
+ }
6617
+ /**
6618
+ * AI-recommended reward for the logged-in customer — "recommended for you"
6619
+ * at the top of the rewards list (requires customerToken). The result is
6620
+ * ALWAYS a real reward from the store's catalog (the AI only ranks; a
6621
+ * hallucinated pick falls back to a deterministic choice). Returns
6622
+ * `{ reward: null }` when the catalog is empty. Rate-limited (5/min per
6623
+ * customer — it spends the merchant's AI credits). Only available in
6624
+ * storefront mode.
6625
+ *
6626
+ * @example
6627
+ * ```typescript
6628
+ * const { reward, reason } = await client.getRecommendedReward();
6629
+ * if (reward) showRecommendation(reward, reason);
6630
+ * ```
6631
+ */
6632
+ async getRecommendedReward() {
6633
+ if (!this.customerToken && !this.proxyMode) {
6634
+ throw new BrainerceError(
6635
+ "Customer token required. Call setCustomerToken() after login.",
6636
+ 401
6637
+ );
6638
+ }
6639
+ if (this.isVibeCodedMode()) {
6640
+ return this.vibeCodedRequest(
6641
+ "GET",
6642
+ "/loyalty/rewards/recommended"
6643
+ );
6644
+ }
6645
+ if (this.storeId && !this.apiKey) {
6646
+ return this.storefrontRequest(
6647
+ "GET",
6648
+ "/loyalty/rewards/recommended"
6649
+ );
6650
+ }
6651
+ throw new BrainerceError(
6652
+ "getRecommendedReward is only available in vibe-coded or storefront mode",
6653
+ 400
6654
+ );
6655
+ }
6656
+ /**
6657
+ * List the paid membership plans the customer can subscribe to (requires
6658
+ * customerToken). Empty when the store offers none or the program is not
6659
+ * active. Only available in storefront mode.
6660
+ *
6661
+ * @example
6662
+ * ```typescript
6663
+ * const plans = await client.getMembershipPlans();
6664
+ * ```
6665
+ */
6666
+ async getMembershipPlans() {
6667
+ if (!this.customerToken && !this.proxyMode) {
6668
+ throw new BrainerceError(
6669
+ "Customer token required. Call setCustomerToken() after login.",
6670
+ 401
6671
+ );
6672
+ }
6673
+ if (this.isVibeCodedMode()) {
6674
+ return this.vibeCodedRequest("GET", "/loyalty/membership/plans");
6675
+ }
6676
+ if (this.storeId && !this.apiKey) {
6677
+ return this.storefrontRequest("GET", "/loyalty/membership/plans");
6678
+ }
6679
+ throw new BrainerceError(
6680
+ "getMembershipPlans is only available in vibe-coded or storefront mode",
6681
+ 400
6682
+ );
6683
+ }
6684
+ /**
6685
+ * List the customer's saved payment methods (display fields only — brand /
6686
+ * last4 / expiry, never card data) for the membership subscribe flow.
6687
+ * Requires customerToken. Cards are vaulted by checking out with
6688
+ * `saveCard: true`. Only available in storefront mode.
6689
+ *
6690
+ * @example
6691
+ * ```typescript
6692
+ * const methods = await client.getMySavedPaymentMethods();
6693
+ * ```
6694
+ */
6695
+ async getMySavedPaymentMethods() {
6696
+ if (!this.customerToken && !this.proxyMode) {
6697
+ throw new BrainerceError(
6698
+ "Customer token required. Call setCustomerToken() after login.",
6699
+ 401
6700
+ );
6701
+ }
6702
+ if (this.isVibeCodedMode()) {
6703
+ return this.vibeCodedRequest(
6704
+ "GET",
6705
+ "/loyalty/membership/payment-methods"
6706
+ );
6707
+ }
6708
+ if (this.storeId && !this.apiKey) {
6709
+ return this.storefrontRequest(
6710
+ "GET",
6711
+ "/loyalty/membership/payment-methods"
6712
+ );
6713
+ }
6714
+ throw new BrainerceError(
6715
+ "getMySavedPaymentMethods is only available in vibe-coded or storefront mode",
6716
+ 400
6717
+ );
6718
+ }
6719
+ /**
6720
+ * Subscribe the customer to a paid membership plan — charges the saved card
6721
+ * IMMEDIATELY and starts the recurring cycle (requires customerToken).
6722
+ * Throws a 409 with a `code` ('card_declined' | 'requires_action' | ...)
6723
+ * when the charge fails; 3D-Secure challenges are not supported off-session
6724
+ * and surface as `requires_action`. Only available in storefront mode.
6725
+ *
6726
+ * @example
6727
+ * ```typescript
6728
+ * const membership = await client.subscribeToMembership({
6729
+ * planId: plan.id,
6730
+ * savedPaymentTokenId: method.id,
6731
+ * });
6732
+ * // membership.status === 'ACTIVE'
6733
+ * ```
6734
+ */
6735
+ async subscribeToMembership(params) {
6736
+ if (!this.customerToken && !this.proxyMode) {
6737
+ throw new BrainerceError(
6738
+ "Customer token required. Call setCustomerToken() after login.",
6739
+ 401
6740
+ );
6741
+ }
6742
+ if (this.isVibeCodedMode()) {
6743
+ return this.vibeCodedRequest(
6744
+ "POST",
6745
+ "/loyalty/membership/subscribe",
6746
+ params
6747
+ );
6748
+ }
6749
+ if (this.storeId && !this.apiKey) {
6750
+ return this.storefrontRequest(
6751
+ "POST",
6752
+ "/loyalty/membership/subscribe",
6753
+ params
6754
+ );
6755
+ }
6756
+ throw new BrainerceError(
6757
+ "subscribeToMembership is only available in vibe-coded or storefront mode",
6758
+ 400
6759
+ );
6760
+ }
6761
+ /**
6762
+ * Cancel the customer's paid membership (requires customerToken).
6763
+ * End-of-period semantics: perks continue until `nextBillingAt`, then the
6764
+ * subscription ends without another charge (a PAST_DUE membership cancels
6765
+ * immediately). Re-subscribing to the same plan before period end simply
6766
+ * un-cancels. Only available in storefront mode.
6767
+ *
6768
+ * @example
6769
+ * ```typescript
6770
+ * const membership = await client.cancelMembership();
6771
+ * // membership.cancelAtPeriodEnd === true
6772
+ * ```
6773
+ */
6774
+ async cancelMembership() {
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("POST", "/loyalty/membership/cancel");
6783
+ }
6784
+ if (this.storeId && !this.apiKey) {
6785
+ return this.storefrontRequest("POST", "/loyalty/membership/cancel");
6786
+ }
6787
+ throw new BrainerceError(
6788
+ "cancelMembership is only available in vibe-coded or storefront mode",
6789
+ 400
6790
+ );
6791
+ }
6792
+ /**
6793
+ * Mint a short-lived session for the embeddable loyalty widget (Phase 5) and
6794
+ * return the ready-to-use iframe URL. Requires customerToken. The returned
6795
+ * `embedUrl` is safe to drop straight into an `<iframe src>` — it carries a
6796
+ * scoped ~15-minute session token, never the real customerToken. Re-call this
6797
+ * before the iframe reloads (e.g. on page navigation) to refresh it.
6798
+ *
6799
+ * @example
6800
+ * ```typescript
6801
+ * const { embedUrl } = await client.getLoyaltyWidgetSession();
6802
+ * // <iframe src={embedUrl} width="360" height="420" />
6803
+ * ```
6804
+ */
6805
+ async getLoyaltyWidgetSession() {
6806
+ if (!this.customerToken && !this.proxyMode) {
6807
+ throw new BrainerceError(
6808
+ "Customer token required. Call setCustomerToken() after login.",
6809
+ 401
6810
+ );
6811
+ }
6812
+ let result;
6813
+ if (this.isVibeCodedMode()) {
6814
+ result = await this.vibeCodedRequest("POST", "/loyalty/widget-session");
6815
+ } else if (this.storeId && !this.apiKey) {
6816
+ result = await this.storefrontRequest("POST", "/loyalty/widget-session");
6817
+ } else {
6818
+ throw new BrainerceError(
6819
+ "getLoyaltyWidgetSession is only available in vibe-coded or storefront mode",
6820
+ 400
6821
+ );
6822
+ }
6823
+ return {
6824
+ ...result,
6825
+ embedUrl: `${this.baseUrl}/api/loyalty/embed/${encodePathSegment(result.storeId)}/${encodePathSegment(result.sessionId)}`
6826
+ };
6539
6827
  }
6540
6828
  /**
6541
6829
  * Get the current customer's orders (requires customerToken)
@@ -8822,6 +9110,247 @@ function formatAmount(amount, currency, locale) {
8822
9110
  }
8823
9111
  }
8824
9112
 
9113
+ // src/jsonld.ts
9114
+ function absoluteUrl(siteUrl, path) {
9115
+ if (!path) return void 0;
9116
+ const base = siteUrl.replace(/\/+$/, "");
9117
+ return path.startsWith("http") ? path : `${base}${path.startsWith("/") ? path : `/${path}`}`;
9118
+ }
9119
+ function stripHtml(html) {
9120
+ return (html ?? "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
9121
+ }
9122
+ function buildArticleJsonLd(post, opts) {
9123
+ const url = absoluteUrl(opts.siteUrl, opts.path ?? `/blog/${post.slug}`);
9124
+ const result = {
9125
+ "@context": "https://schema.org",
9126
+ "@type": "Article",
9127
+ headline: post.seoTitle ?? post.title,
9128
+ name: post.title,
9129
+ ...post.excerpt || post.seoDescription ? { description: post.seoDescription ?? post.excerpt } : {},
9130
+ ...url ? { url, mainEntityOfPage: { "@type": "WebPage", "@id": url } } : {},
9131
+ ...post.coverImageUrl ? { image: [post.coverImageUrl] } : {},
9132
+ ...post.publishedAt ? { datePublished: post.publishedAt } : {},
9133
+ dateModified: post.updatedAt,
9134
+ ...post.author ? { author: { "@type": "Person", name: post.author } } : opts.organizationName ? { author: { "@type": "Organization", name: opts.organizationName } } : {},
9135
+ ...opts.organizationName ? { publisher: { "@type": "Organization", name: opts.organizationName } } : {},
9136
+ ...post.tags.length > 0 ? { keywords: post.tags.join(", ") } : {}
9137
+ };
9138
+ return result;
9139
+ }
9140
+ function buildProductJsonLd(product, opts) {
9141
+ const url = absoluteUrl(
9142
+ opts.siteUrl,
9143
+ opts.path ?? (product.slug ? `/products/${product.slug}` : void 0)
9144
+ );
9145
+ const images = (product.images ?? []).map((img) => img.url).filter(Boolean);
9146
+ const brand = opts.brandName ?? product.brands?.[0]?.name;
9147
+ const description = stripHtml(product.description).slice(0, 5e3);
9148
+ const effectivePrice = product.salePrice ?? product.basePrice;
9149
+ const inStock = product.inventory ? (product.inventory.available ?? 0) > 0 : true;
9150
+ const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
9151
+ const itemCondition = "https://schema.org/NewCondition";
9152
+ const shippingDetails = (opts.shipping ?? []).filter((z) => z.amount !== null).map((z) => ({
9153
+ "@type": "OfferShippingDetails",
9154
+ shippingRate: { "@type": "MonetaryAmount", value: z.amount, currency: opts.currency },
9155
+ shippingDestination: { "@type": "DefinedRegion", addressCountry: z.countries },
9156
+ ...z.handlingTime != null || z.minDeliveryDays != null || z.maxDeliveryDays != null ? {
9157
+ deliveryTime: {
9158
+ "@type": "ShippingDeliveryTime",
9159
+ ...z.handlingTime != null ? {
9160
+ handlingTime: {
9161
+ "@type": "QuantitativeValue",
9162
+ minValue: 0,
9163
+ maxValue: z.handlingTime
9164
+ }
9165
+ } : {},
9166
+ ...z.minDeliveryDays != null || z.maxDeliveryDays != null ? {
9167
+ transitTime: {
9168
+ "@type": "QuantitativeValue",
9169
+ minValue: z.minDeliveryDays ?? z.maxDeliveryDays,
9170
+ maxValue: z.maxDeliveryDays ?? z.minDeliveryDays
9171
+ }
9172
+ } : {}
9173
+ }
9174
+ } : {}
9175
+ }));
9176
+ const offer = isVariable ? {
9177
+ "@type": "AggregateOffer",
9178
+ lowPrice: product.priceMin,
9179
+ highPrice: product.priceMax,
9180
+ priceCurrency: opts.currency,
9181
+ availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
9182
+ itemCondition,
9183
+ ...shippingDetails.length > 0 ? { shippingDetails } : {},
9184
+ ...url ? { url } : {}
9185
+ } : {
9186
+ "@type": "Offer",
9187
+ price: effectivePrice,
9188
+ priceCurrency: opts.currency,
9189
+ availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
9190
+ itemCondition,
9191
+ // Only meaningful for an active sale price with a known end date —
9192
+ // a regular (non-sale) price has no expiry to declare.
9193
+ ...product.salePrice && product.salePriceEndsAt ? { priceValidUntil: product.salePriceEndsAt } : {},
9194
+ ...shippingDetails.length > 0 ? { shippingDetails } : {},
9195
+ ...url ? { url } : {}
9196
+ };
9197
+ return {
9198
+ "@context": "https://schema.org",
9199
+ "@type": "Product",
9200
+ name: product.name,
9201
+ ...description ? { description } : {},
9202
+ ...images.length > 0 ? { image: images } : {},
9203
+ ...url ? { url } : {},
9204
+ sku: product.sku,
9205
+ // Identifiers Google uses to match a product to its catalog — key for
9206
+ // free merchant-listing eligibility without a Merchant Center feed.
9207
+ ...product.gtin ? { gtin: product.gtin } : {},
9208
+ ...product.mpn ? { mpn: product.mpn } : {},
9209
+ ...brand ? { brand: { "@type": "Brand", name: brand } } : {},
9210
+ offers: offer,
9211
+ // Google policy: never emit an empty/zero rating block.
9212
+ ...product.reviewCount && product.reviewCount > 0 && product.avgRating ? {
9213
+ aggregateRating: {
9214
+ "@type": "AggregateRating",
9215
+ ratingValue: product.avgRating,
9216
+ reviewCount: product.reviewCount
9217
+ }
9218
+ } : {}
9219
+ };
9220
+ }
9221
+ function buildOrganizationJsonLd(store, opts) {
9222
+ const sameAs = Object.values(store.socialLinks ?? {}).filter(Boolean);
9223
+ return {
9224
+ "@context": "https://schema.org",
9225
+ "@type": "Organization",
9226
+ name: store.name,
9227
+ url: opts.siteUrl,
9228
+ ...store.logo ? { logo: store.logo } : {},
9229
+ ...store.metaDescription ? { description: store.metaDescription } : {},
9230
+ ...store.contactEmail ? { email: store.contactEmail } : {},
9231
+ ...store.contactPhone ? { telephone: store.contactPhone } : {},
9232
+ ...sameAs.length > 0 ? { sameAs } : {}
9233
+ };
9234
+ }
9235
+ function buildWebsiteJsonLd(store, opts) {
9236
+ const base = opts.siteUrl.replace(/\/+$/, "");
9237
+ const result = {
9238
+ "@context": "https://schema.org",
9239
+ "@type": "WebSite",
9240
+ name: store.name,
9241
+ url: opts.siteUrl
9242
+ };
9243
+ if (opts.searchUrlTemplate?.includes("{search_term_string}")) {
9244
+ const tmpl = opts.searchUrlTemplate.startsWith("http") ? opts.searchUrlTemplate : `${base}${opts.searchUrlTemplate.startsWith("/") ? "" : "/"}${opts.searchUrlTemplate}`;
9245
+ result.potentialAction = {
9246
+ "@type": "SearchAction",
9247
+ target: { "@type": "EntryPoint", urlTemplate: tmpl },
9248
+ "query-input": "required name=search_term_string"
9249
+ };
9250
+ }
9251
+ return result;
9252
+ }
9253
+ function buildCollectionPageJsonLd(category, opts) {
9254
+ const url = absoluteUrl(opts.siteUrl, opts.path);
9255
+ const description = category.metaDescription ?? (stripHtml(category.description) || void 0);
9256
+ return {
9257
+ "@context": "https://schema.org",
9258
+ "@type": "CollectionPage",
9259
+ name: category.name,
9260
+ ...description ? { description } : {},
9261
+ ...url ? { url, mainEntityOfPage: { "@type": "WebPage", "@id": url } } : {},
9262
+ ...category.image ? { image: [category.image] } : {}
9263
+ };
9264
+ }
9265
+ function buildBreadcrumbJsonLd(items) {
9266
+ return {
9267
+ "@context": "https://schema.org",
9268
+ "@type": "BreadcrumbList",
9269
+ itemListElement: items.map((item, index) => ({
9270
+ "@type": "ListItem",
9271
+ position: index + 1,
9272
+ name: item.name,
9273
+ item: item.url
9274
+ }))
9275
+ };
9276
+ }
9277
+ function jsonLdScriptProps(data) {
9278
+ return {
9279
+ type: "application/ld+json",
9280
+ dangerouslySetInnerHTML: { __html: JSON.stringify(data).replace(/</g, "\\u003c") }
9281
+ };
9282
+ }
9283
+
9284
+ // src/sitemap.ts
9285
+ async function getBlogSitemapEntries(client, opts) {
9286
+ const base = opts.siteUrl.replace(/\/+$/, "");
9287
+ const basePath = opts.basePath ?? "/blog";
9288
+ const pageSize = Math.min(opts.pageSize ?? 100, 100);
9289
+ const maxEntries = opts.maxEntries ?? 5e3;
9290
+ const posts = [];
9291
+ let page = 1;
9292
+ for (; ; ) {
9293
+ const res = await client.blog.getPosts({ page, limit: pageSize });
9294
+ posts.push(...res.data.map((p) => ({ slug: p.slug, updatedAt: p.updatedAt })));
9295
+ if (page >= res.meta.totalPages || posts.length >= maxEntries) break;
9296
+ page += 1;
9297
+ }
9298
+ const nonDefaultLocales = opts.locales?.filter((locale) => locale !== opts.defaultLocale) ?? [];
9299
+ const entries = [];
9300
+ entries.push({ url: `${base}${basePath}`, changeFrequency: "daily", priority: 0.7 });
9301
+ for (const locale of nonDefaultLocales) {
9302
+ entries.push({
9303
+ url: `${base}/${locale}${basePath}`,
9304
+ changeFrequency: "daily",
9305
+ priority: 0.7
9306
+ });
9307
+ }
9308
+ for (const post of posts.slice(0, maxEntries)) {
9309
+ const lastModified = new Date(post.updatedAt);
9310
+ entries.push({
9311
+ url: `${base}${basePath}/${post.slug}`,
9312
+ lastModified,
9313
+ changeFrequency: "weekly",
9314
+ priority: 0.6
9315
+ });
9316
+ for (const locale of nonDefaultLocales) {
9317
+ entries.push({
9318
+ url: `${base}/${locale}${basePath}/${post.slug}`,
9319
+ lastModified,
9320
+ changeFrequency: "weekly",
9321
+ priority: 0.5
9322
+ });
9323
+ }
9324
+ }
9325
+ return entries;
9326
+ }
9327
+ async function getCategorySitemapEntries(client, opts) {
9328
+ const base = opts.siteUrl.replace(/\/+$/, "");
9329
+ const basePath = opts.basePath ?? "/category";
9330
+ const nonDefaultLocales = opts.locales?.filter((l) => l !== opts.defaultLocale) ?? [];
9331
+ const { categories } = await client.getCategories();
9332
+ const slugs = [];
9333
+ const walk = (nodes) => {
9334
+ for (const node of nodes) {
9335
+ if (node.slug) slugs.push(node.slug);
9336
+ if (node.children?.length) walk(node.children);
9337
+ }
9338
+ };
9339
+ walk(categories);
9340
+ const entries = [];
9341
+ for (const slug of slugs) {
9342
+ entries.push({ url: `${base}${basePath}/${slug}`, changeFrequency: "daily", priority: 0.8 });
9343
+ for (const locale of nonDefaultLocales) {
9344
+ entries.push({
9345
+ url: `${base}/${locale}${basePath}/${slug}`,
9346
+ changeFrequency: "daily",
9347
+ priority: 0.7
9348
+ });
9349
+ }
9350
+ }
9351
+ return entries;
9352
+ }
9353
+
8825
9354
  // src/types.ts
8826
9355
  function isHtmlDescription(product) {
8827
9356
  if (product?.descriptionFormat === "html") return true;
@@ -8837,7 +9366,7 @@ function getDescriptionContent(product) {
8837
9366
  }
8838
9367
  return { text: product.description };
8839
9368
  }
8840
- function stripHtml(html) {
9369
+ function stripHtml2(html) {
8841
9370
  if (!html) return "";
8842
9371
  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();
8843
9372
  }
@@ -8846,7 +9375,7 @@ function deriveSeoDescription(product, options) {
8846
9375
  const maxLength = options?.maxLength ?? 160;
8847
9376
  const authored = (product.seoDescription ?? product.metaDescription ?? "").trim();
8848
9377
  if (authored) return authored;
8849
- const plain = stripHtml(product.description);
9378
+ const plain = stripHtml2(product.description);
8850
9379
  if (plain) {
8851
9380
  if (plain.length <= maxLength) return plain;
8852
9381
  const sliceLength = maxLength - 1;
@@ -9067,6 +9596,12 @@ export {
9067
9596
  BrainerceError,
9068
9597
  RTL_LOCALES,
9069
9598
  SDK_VERSION,
9599
+ buildArticleJsonLd,
9600
+ buildBreadcrumbJsonLd,
9601
+ buildCollectionPageJsonLd,
9602
+ buildOrganizationJsonLd,
9603
+ buildProductJsonLd,
9604
+ buildWebsiteJsonLd,
9070
9605
  createWebhookHandler,
9071
9606
  deriveSeoDescription,
9072
9607
  enableDevGuards,
@@ -9074,9 +9609,11 @@ export {
9074
9609
  formatPrice,
9075
9610
  formatProductPrice,
9076
9611
  formatVariantPrice,
9612
+ getBlogSitemapEntries,
9077
9613
  getCartItemImage,
9078
9614
  getCartItemName,
9079
9615
  getCartTotals,
9616
+ getCategorySitemapEntries,
9080
9617
  getDescriptionContent,
9081
9618
  getDirectionForLocale,
9082
9619
  formatPrice as getPriceDisplay,
@@ -9094,8 +9631,9 @@ export {
9094
9631
  isCouponApplicableToProduct,
9095
9632
  isHtmlDescription,
9096
9633
  isWebhookEventType,
9634
+ jsonLdScriptProps,
9097
9635
  parseWebhookEvent,
9098
9636
  safePaymentRedirect,
9099
- stripHtml,
9637
+ stripHtml2 as stripHtml,
9100
9638
  verifyWebhook
9101
9639
  };