brainerce 1.54.0 → 1.56.0

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
@@ -63,6 +63,7 @@ __export(index_exports, {
63
63
  getProductMetafieldsByType: () => getProductMetafieldsByType,
64
64
  getProductPrice: () => getProductPrice,
65
65
  getProductPriceInfo: () => getProductPriceInfo,
66
+ getProductSitemapEntries: () => getProductSitemapEntries,
66
67
  getProductSwatches: () => getProductSwatches,
67
68
  getStockStatus: () => getStockStatus,
68
69
  getVariantOptions: () => getVariantOptions,
@@ -206,6 +207,24 @@ var SDK_VERSION = "1.54.0";
206
207
  // src/client.ts
207
208
  var DEFAULT_BASE_URL = "https://api.brainerce.com";
208
209
  var DEFAULT_TIMEOUT = 3e4;
210
+ var META_EVENT_NAMES = {
211
+ view_item: "ViewContent",
212
+ add_to_cart: "AddToCart",
213
+ begin_checkout: "InitiateCheckout",
214
+ add_payment_info: "AddPaymentInfo",
215
+ purchase: "Purchase",
216
+ search: "Search",
217
+ sign_up: "CompleteRegistration"
218
+ };
219
+ var TIKTOK_EVENT_NAMES = {
220
+ view_item: "ViewContent",
221
+ add_to_cart: "AddToCart",
222
+ begin_checkout: "InitiateCheckout",
223
+ add_payment_info: "AddPaymentInfo",
224
+ purchase: "CompletePayment",
225
+ search: "Search",
226
+ sign_up: "CompleteRegistration"
227
+ };
209
228
  var RTL_LOCALES = /* @__PURE__ */ new Set(["ar", "he", "fa", "ur", "yi"]);
210
229
  function getDirectionForLocale(locale) {
211
230
  if (!locale) return "ltr";
@@ -260,6 +279,12 @@ var _BrainerceClient = class _BrainerceClient {
260
279
  // GA4 stitch state (see `loadGoogleAnalytics()` in the Analytics section).
261
280
  this._ga4MeasurementId = null;
262
281
  this._ga4StitchPromise = null;
282
+ // Marketing tags booted by `initTracking()`. Each holds the id already
283
+ // loaded, which is what makes a repeat call a no-op instead of a second
284
+ // pixel install (and a doubled PageView).
285
+ this._gtmContainerId = null;
286
+ this._metaPixelId = null;
287
+ this._tiktokPixelId = null;
263
288
  /** One warning per client, not per keystroke-driven address submit. */
264
289
  this._warnedResolvedOnlyAddressFields = false;
265
290
  /** localStorage key for session cart reference (sessionToken + cartId) */
@@ -1281,6 +1306,225 @@ var _BrainerceClient = class _BrainerceClient {
1281
1306
  }
1282
1307
  });
1283
1308
  }
1309
+ /**
1310
+ * Boot every marketing tag the merchant has configured — GA4, Google Tag
1311
+ * Manager, the Meta pixel, the TikTok pixel — in one call.
1312
+ *
1313
+ * Pass `storeInfo.tracking` straight through. The ids in it are resolved
1314
+ * server-side from the marketplace apps the merchant already connected, so
1315
+ * for the common case nobody types an id anywhere and nobody redeploys the
1316
+ * storefront: connect the Google app in the dashboard and this call starts
1317
+ * loading GA4 on the next page render.
1318
+ *
1319
+ * Call it once, as early as possible (root layout / app entry). It is
1320
+ * idempotent, a no-op during SSR, and never throws — a blocked or missing
1321
+ * tag must never take a storefront down with it.
1322
+ *
1323
+ * GA4 goes through {@link loadGoogleAnalytics}, so the `client_id` /
1324
+ * `session_id` stitch ids keep flowing onto cart and checkout calls and the
1325
+ * server-side purchase conversion still lands in the right session.
1326
+ *
1327
+ * @example
1328
+ * ```typescript
1329
+ * const storeInfo = await client.getStoreInfo();
1330
+ * client.initTracking(storeInfo.tracking);
1331
+ * // …later, on the order confirmation page:
1332
+ * client.trackMarketingEvent('purchase', {
1333
+ * transactionId: order.id,
1334
+ * currency: order.currency,
1335
+ * value: order.totalAmount,
1336
+ * items: order.items.map((i) => ({ itemId: i.sku, itemName: i.name, price: i.price, quantity: i.quantity })),
1337
+ * });
1338
+ * ```
1339
+ */
1340
+ initTracking(tracking) {
1341
+ if (typeof window === "undefined" || !tracking) return;
1342
+ if (tracking.ga4MeasurementId) {
1343
+ this.loadGoogleAnalytics(tracking.ga4MeasurementId);
1344
+ }
1345
+ if (tracking.gtmContainerId) this.loadGtm(tracking.gtmContainerId);
1346
+ if (tracking.metaPixelId) this.loadMetaPixel(tracking.metaPixelId);
1347
+ if (tracking.tiktokPixelId) this.loadTikTokPixel(tracking.tiktokPixelId);
1348
+ }
1349
+ /**
1350
+ * Report one e-commerce event to every marketing tag that
1351
+ * {@link initTracking} loaded (GA4/GTM, Meta, TikTok).
1352
+ *
1353
+ * Distinct from {@link trackEvent}, which posts a cookieless pageview/beacon
1354
+ * to Brainerce's own storefront analytics. This one is about ad platforms —
1355
+ * call both; they answer different questions.
1356
+ *
1357
+ * You describe what happened once, in GA4's vocabulary, and the SDK
1358
+ * translates: a `dataLayer` push for GA4/GTM, the matching Meta standard
1359
+ * event via `fbq`, and the matching TikTok event via `ttq`. Tags that aren't
1360
+ * loaded are skipped silently, so the same call is correct whether the
1361
+ * merchant has connected none, one, or all of them.
1362
+ *
1363
+ * Why this matters for ad spend: a GTM container with no `dataLayer` events
1364
+ * is an empty container, and Meta cannot optimize a campaign it never sees a
1365
+ * `Purchase` for. The value/currency/item-id triple in {@link TrackingEventPayload}
1366
+ * is the whole input to that optimization.
1367
+ *
1368
+ * `purchase` is de-duplicated by the vendors on `transactionId` (GA4
1369
+ * `transaction_id`, Meta `eventID`, TikTok `event_id`), so a shopper
1370
+ * refreshing the confirmation page cannot double-count the order — pass the
1371
+ * order id and the safety is automatic.
1372
+ *
1373
+ * SSR-safe and never throws.
1374
+ */
1375
+ trackMarketingEvent(name, payload = {}) {
1376
+ if (typeof window === "undefined") return;
1377
+ try {
1378
+ const items = payload.items ?? [];
1379
+ window.dataLayer = window.dataLayer || [];
1380
+ window.dataLayer.push({ ecommerce: null });
1381
+ window.dataLayer.push({
1382
+ event: name,
1383
+ ecommerce: {
1384
+ ...payload.currency ? { currency: payload.currency } : {},
1385
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1386
+ ...payload.transactionId ? { transaction_id: payload.transactionId } : {},
1387
+ ...payload.shipping !== void 0 ? { shipping: payload.shipping } : {},
1388
+ ...payload.tax !== void 0 ? { tax: payload.tax } : {},
1389
+ ...payload.coupon ? { coupon: payload.coupon } : {},
1390
+ items: items.map((item) => ({
1391
+ item_id: item.itemId,
1392
+ ...item.itemName ? { item_name: item.itemName } : {},
1393
+ ...item.price !== void 0 ? { price: item.price } : {},
1394
+ ...item.quantity !== void 0 ? { quantity: item.quantity } : {},
1395
+ ...item.itemVariant ? { item_variant: item.itemVariant } : {},
1396
+ ...item.itemCategory ? { item_category: item.itemCategory } : {}
1397
+ }))
1398
+ }
1399
+ });
1400
+ if (window.gtag && this._ga4MeasurementId) {
1401
+ window.gtag("event", name, {
1402
+ ...payload.currency ? { currency: payload.currency } : {},
1403
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1404
+ ...payload.transactionId ? { transaction_id: payload.transactionId } : {},
1405
+ ...payload.shipping !== void 0 ? { shipping: payload.shipping } : {},
1406
+ ...payload.tax !== void 0 ? { tax: payload.tax } : {},
1407
+ ...payload.coupon ? { coupon: payload.coupon } : {},
1408
+ items: items.map((item) => ({
1409
+ item_id: item.itemId,
1410
+ item_name: item.itemName,
1411
+ price: item.price,
1412
+ quantity: item.quantity
1413
+ }))
1414
+ });
1415
+ }
1416
+ const metaEvent = META_EVENT_NAMES[name];
1417
+ if (window.fbq && metaEvent) {
1418
+ const contents = items.map((item) => ({
1419
+ id: item.itemId,
1420
+ quantity: item.quantity ?? 1,
1421
+ ...item.price !== void 0 ? { item_price: item.price } : {}
1422
+ }));
1423
+ window.fbq(
1424
+ "track",
1425
+ metaEvent,
1426
+ {
1427
+ ...payload.currency ? { currency: payload.currency } : {},
1428
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1429
+ ...contents.length ? { contents, content_ids: contents.map((c) => c.id), content_type: "product" } : {}
1430
+ },
1431
+ // Meta de-dupes a browser event against a server (CAPI) event of the
1432
+ // same eventID, and against a repeat send of the same page.
1433
+ payload.transactionId ? { eventID: payload.transactionId } : void 0
1434
+ );
1435
+ }
1436
+ const tiktokEvent = TIKTOK_EVENT_NAMES[name];
1437
+ if (window.ttq?.track && tiktokEvent) {
1438
+ window.ttq.track(
1439
+ tiktokEvent,
1440
+ {
1441
+ ...payload.currency ? { currency: payload.currency } : {},
1442
+ ...payload.value !== void 0 ? { value: payload.value } : {},
1443
+ contents: items.map((item) => ({
1444
+ content_id: item.itemId,
1445
+ content_name: item.itemName,
1446
+ quantity: item.quantity ?? 1,
1447
+ price: item.price
1448
+ }))
1449
+ },
1450
+ payload.transactionId ? { event_id: payload.transactionId } : void 0
1451
+ );
1452
+ }
1453
+ } catch {
1454
+ }
1455
+ }
1456
+ /** Install the GTM container loader. Idempotent; no-op if already present. */
1457
+ loadGtm(containerId) {
1458
+ if (this._gtmContainerId === containerId) return;
1459
+ this._gtmContainerId = containerId;
1460
+ try {
1461
+ window.dataLayer = window.dataLayer || [];
1462
+ window.dataLayer.push({ "gtm.start": Date.now(), event: "gtm.js" });
1463
+ const script = document.createElement("script");
1464
+ script.async = true;
1465
+ script.src = `https://www.googletagmanager.com/gtm.js?id=${encodeURIComponent(containerId)}`;
1466
+ document.head.appendChild(script);
1467
+ } catch {
1468
+ }
1469
+ }
1470
+ /** Install the Meta pixel and fire its initial PageView. Idempotent. */
1471
+ loadMetaPixel(pixelId) {
1472
+ if (this._metaPixelId === pixelId) return;
1473
+ this._metaPixelId = pixelId;
1474
+ try {
1475
+ if (!window.fbq) {
1476
+ const queue = [];
1477
+ const fbq = ((...args) => {
1478
+ if (fbq.callMethod) fbq.callMethod(...args);
1479
+ else queue.push(args);
1480
+ });
1481
+ fbq.queue = queue;
1482
+ fbq.loaded = true;
1483
+ fbq.version = "2.0";
1484
+ window.fbq = fbq;
1485
+ window._fbq = fbq;
1486
+ const script = document.createElement("script");
1487
+ script.async = true;
1488
+ script.src = "https://connect.facebook.net/en_US/fbevents.js";
1489
+ document.head.appendChild(script);
1490
+ }
1491
+ window.fbq("init", pixelId);
1492
+ window.fbq("track", "PageView");
1493
+ } catch {
1494
+ }
1495
+ }
1496
+ /** Install the TikTok pixel and fire its initial page view. Idempotent. */
1497
+ loadTikTokPixel(pixelId) {
1498
+ if (this._tiktokPixelId === pixelId) return;
1499
+ this._tiktokPixelId = pixelId;
1500
+ try {
1501
+ const ttq = window.ttq ?? {};
1502
+ ttq._i = ttq._i ?? {};
1503
+ ttq._i[pixelId] = ttq._i[pixelId] ?? [];
1504
+ ttq._t = ttq._t ?? {};
1505
+ ttq._t[pixelId] = Date.now();
1506
+ ttq._o = ttq._o ?? {};
1507
+ ttq._o[pixelId] = {};
1508
+ const methods = ["page", "track", "identify", "instances", "ready"];
1509
+ ttq.methods = methods;
1510
+ for (const method of methods) {
1511
+ if (typeof ttq[method] !== "function") {
1512
+ ttq[method] = (...args) => {
1513
+ ttq._i?.[pixelId]?.push([method, ...args]);
1514
+ };
1515
+ }
1516
+ }
1517
+ window.ttq = ttq;
1518
+ const script = document.createElement("script");
1519
+ script.async = true;
1520
+ script.src = `https://analytics.tiktok.com/i18n/pixel/events.js?sdkid=${encodeURIComponent(
1521
+ pixelId
1522
+ )}&lib=ttq`;
1523
+ document.head.appendChild(script);
1524
+ ttq.page?.();
1525
+ } catch {
1526
+ }
1527
+ }
1284
1528
  /**
1285
1529
  * Merge the resolved GA4 stitch ids onto a request body — only for fields
1286
1530
  * the caller didn't already set explicitly (explicit values always win).
@@ -1410,6 +1654,50 @@ var _BrainerceClient = class _BrainerceClient {
1410
1654
  queryParamsWithRegion
1411
1655
  );
1412
1656
  }
1657
+ /**
1658
+ * Lightweight product rows for sitemap generation: `slug`, `updatedAt`, and
1659
+ * per-locale `localeSlugs` only — up to `limit` (max 5000) in ONE request,
1660
+ * with none of the 100-per-page clamp the full listing applies.
1661
+ *
1662
+ * Sales-channel (`salesChannelId`) mode only; other modes throw so
1663
+ * {@link getProductSitemapEntries} (sitemap.ts) can catch and fall back to
1664
+ * paginating `getProducts`. Prefer that helper over calling this directly.
1665
+ */
1666
+ async getSitemapProducts(limit = 5e3) {
1667
+ if (!this.isVibeCodedMode()) {
1668
+ throw new Error("getSitemapProducts is available in salesChannelId mode only");
1669
+ }
1670
+ const res = await this.vibeCodedRequest("GET", "/sitemap-products", void 0, { limit: Math.min(limit, 5e3) });
1671
+ return res.data;
1672
+ }
1673
+ /**
1674
+ * Resolve an old (renamed) slug to the entity's CURRENT slug, so the
1675
+ * storefront can issue a permanent (301/308) redirect instead of a 404.
1676
+ *
1677
+ * Call this in the catch/not-found path of a product or blog page:
1678
+ * the platform records every slug rename, so a URL that stopped matching
1679
+ * usually has a redirect. Returns `null` when there is no redirect (real
1680
+ * 404) or outside sales-channel mode — always fall through to notFound().
1681
+ *
1682
+ * @example
1683
+ * ```typescript
1684
+ * // app/products/[slug]/page.tsx — in the catch path:
1685
+ * const redirect = await client.resolveSlugRedirect('product', slug);
1686
+ * if (redirect) permanentRedirect(`/products/${redirect.currentSlug}`);
1687
+ * notFound();
1688
+ * ```
1689
+ */
1690
+ async resolveSlugRedirect(entityType, slug) {
1691
+ if (!this.isVibeCodedMode()) return null;
1692
+ try {
1693
+ return await this.vibeCodedRequest(
1694
+ "GET",
1695
+ `/slug-redirects/${entityType}/${encodePathSegment(slug)}`
1696
+ );
1697
+ } catch {
1698
+ return null;
1699
+ }
1700
+ }
1413
1701
  /**
1414
1702
  * Get a single product by ID
1415
1703
  * Works in vibe-coded, storefront (public), and admin mode
@@ -5201,9 +5489,7 @@ var _BrainerceClient = class _BrainerceClient {
5201
5489
  * ```
5202
5490
  */
5203
5491
  async setShippingAddress(checkoutId, address) {
5204
- const body = await this.withAnalyticsStitchIds(
5205
- this.stripResolvedOnlyAddressFields(address)
5206
- );
5492
+ const body = await this.withAnalyticsStitchIds(this.stripResolvedOnlyAddressFields(address));
5207
5493
  if (this.isVibeCodedMode()) {
5208
5494
  return this.vibeCodedRequest(
5209
5495
  "PATCH",
@@ -8704,6 +8990,41 @@ var _BrainerceClient = class _BrainerceClient {
8704
8990
  { salesChannelId }
8705
8991
  );
8706
8992
  }
8993
+ /**
8994
+ * Attach a customer to a sales channel (admin mode) — marks them as active in
8995
+ * that storefront. Accepts the sales-channel record ID or its public `vc_*`
8996
+ * connection ID.
8997
+ *
8998
+ * Rarely needed: the platform records a channel by itself whenever the
8999
+ * customer registers, signs in or checks out on it. Use this for migrations
9000
+ * from another system and for fixing up records you created yourself. A
9001
+ * customer belongs to one store but can be active in any number of its
9002
+ * channels, so calling this for several channels is normal and expected.
9003
+ *
9004
+ * This does not change where the customer CAME FROM — for that, pass
9005
+ * `acquisitionSalesChannelId` to {@link updateCustomer}.
9006
+ */
9007
+ async publishCustomerToSalesChannel(customerId, salesChannelId) {
9008
+ return this.adminRequest(
9009
+ "POST",
9010
+ `/api/v1/customers/${encodePathSegment(customerId)}/publish-sales-channel`,
9011
+ { salesChannelId }
9012
+ );
9013
+ }
9014
+ /**
9015
+ * Detach a customer from a sales channel (admin mode).
9016
+ *
9017
+ * A correction, NOT a block — it does not stop that person from buying on
9018
+ * that storefront, and the channel is recorded again the next time they sign
9019
+ * in or order there. There is no way to bar a customer from a channel.
9020
+ */
9021
+ async unpublishCustomerFromSalesChannel(customerId, salesChannelId) {
9022
+ return this.adminRequest(
9023
+ "POST",
9024
+ `/api/v1/customers/${encodePathSegment(customerId)}/unpublish-sales-channel`,
9025
+ { salesChannelId }
9026
+ );
9027
+ }
8707
9028
  /**
8708
9029
  * Publish a coupon to a sales channel (admin mode) — makes it redeemable on
8709
9030
  * that vibe-coded storefront. Accepts the sales-channel record ID or its
@@ -9895,7 +10216,8 @@ function buildProductJsonLd(product, opts) {
9895
10216
  const brand = opts.brandName ?? product.brands?.[0]?.name;
9896
10217
  const description = stripHtml(product.description).slice(0, 5e3);
9897
10218
  const effectivePrice = product.salePrice ?? product.basePrice;
9898
- const inStock = product.inventory ? (product.inventory.available ?? 0) > 0 : true;
10219
+ const inv = product.inventory;
10220
+ const availability = !inv ? "https://schema.org/InStock" : inv.inStock ?? (inv.available ?? 0) > 0 ? "https://schema.org/InStock" : inv.canPurchase ? "https://schema.org/BackOrder" : "https://schema.org/OutOfStock";
9899
10221
  const isVariable = product.type === "VARIABLE" && product.priceMin && product.priceMax;
9900
10222
  const itemCondition = "https://schema.org/NewCondition";
9901
10223
  const shippingDetails = (opts.shipping ?? []).filter((z) => z.amount !== null).map((z) => ({
@@ -9926,8 +10248,9 @@ function buildProductJsonLd(product, opts) {
9926
10248
  "@type": "AggregateOffer",
9927
10249
  lowPrice: product.priceMin,
9928
10250
  highPrice: product.priceMax,
10251
+ ...product.variants?.length ? { offerCount: product.variants.length } : {},
9929
10252
  priceCurrency: opts.currency,
9930
- availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
10253
+ availability,
9931
10254
  itemCondition,
9932
10255
  ...shippingDetails.length > 0 ? { shippingDetails } : {},
9933
10256
  ...url ? { url } : {}
@@ -9935,7 +10258,7 @@ function buildProductJsonLd(product, opts) {
9935
10258
  "@type": "Offer",
9936
10259
  price: effectivePrice,
9937
10260
  priceCurrency: opts.currency,
9938
- availability: inStock ? "https://schema.org/InStock" : "https://schema.org/OutOfStock",
10261
+ availability,
9939
10262
  itemCondition,
9940
10263
  // Only meaningful for an active sale price with a known end date —
9941
10264
  // a regular (non-sale) price has no expiry to declare.
@@ -9950,19 +10273,25 @@ function buildProductJsonLd(product, opts) {
9950
10273
  ...description ? { description } : {},
9951
10274
  ...images.length > 0 ? { image: images } : {},
9952
10275
  ...url ? { url } : {},
9953
- sku: product.sku,
10276
+ // Fall back to the product id so the offer always carries a stable SKU —
10277
+ // Merchant Center matching and review aggregation both key on it.
10278
+ sku: product.sku || product.id,
9954
10279
  // Identifiers Google uses to match a product to its catalog — key for
9955
10280
  // free merchant-listing eligibility without a Merchant Center feed.
9956
10281
  ...product.gtin ? { gtin: product.gtin } : {},
9957
10282
  ...product.mpn ? { mpn: product.mpn } : {},
9958
10283
  ...brand ? { brand: { "@type": "Brand", name: brand } } : {},
9959
10284
  offers: offer,
9960
- // Google policy: never emit an empty/zero rating block.
10285
+ // Google policy: never emit an empty/zero rating block. bestRating /
10286
+ // worstRating make the 1-5 scale explicit so aggregators can't misread
10287
+ // a 4.8 on an assumed 0-10 scale.
9961
10288
  ...product.reviewCount && product.reviewCount > 0 && product.avgRating ? {
9962
10289
  aggregateRating: {
9963
10290
  "@type": "AggregateRating",
9964
10291
  ratingValue: product.avgRating,
9965
- reviewCount: product.reviewCount
10292
+ reviewCount: product.reviewCount,
10293
+ bestRating: 5,
10294
+ worstRating: 1
9966
10295
  }
9967
10296
  } : {}
9968
10297
  };
@@ -10073,6 +10402,54 @@ async function getBlogSitemapEntries(client, opts) {
10073
10402
  }
10074
10403
  return entries;
10075
10404
  }
10405
+ async function getProductSitemapEntries(client, opts) {
10406
+ const base = opts.siteUrl.replace(/\/+$/, "");
10407
+ const basePath = opts.basePath ?? "/products";
10408
+ const pageSize = Math.min(opts.pageSize ?? 100, 100);
10409
+ const maxEntries = opts.maxEntries ?? 5e3;
10410
+ let rows = [];
10411
+ try {
10412
+ rows = await client.getSitemapProducts(maxEntries);
10413
+ } catch {
10414
+ let page = 1;
10415
+ for (; ; ) {
10416
+ const res = await client.getProducts({ page, limit: pageSize });
10417
+ rows.push(
10418
+ ...res.data.map((p) => ({
10419
+ slug: p.slug ?? null,
10420
+ id: p.id,
10421
+ updatedAt: p.updatedAt,
10422
+ localeSlugs: p.localeSlugs ?? null
10423
+ }))
10424
+ );
10425
+ if (page >= res.meta.totalPages || rows.length >= maxEntries) break;
10426
+ page += 1;
10427
+ }
10428
+ }
10429
+ const nonDefaultLocales = opts.locales?.filter((locale) => locale !== opts.defaultLocale) ?? [];
10430
+ const entries = [];
10431
+ for (const row of rows.slice(0, maxEntries)) {
10432
+ const baseSlug = row.slug || row.id;
10433
+ if (!baseSlug) continue;
10434
+ const lastModified = row.updatedAt ? new Date(row.updatedAt) : void 0;
10435
+ const localeSlugs = row.localeSlugs ?? {};
10436
+ entries.push({
10437
+ url: `${base}${basePath}/${baseSlug}`,
10438
+ ...lastModified ? { lastModified } : {},
10439
+ changeFrequency: "daily",
10440
+ priority: 0.8
10441
+ });
10442
+ for (const locale of nonDefaultLocales) {
10443
+ entries.push({
10444
+ url: `${base}/${locale}${basePath}/${localeSlugs[locale] || baseSlug}`,
10445
+ ...lastModified ? { lastModified } : {},
10446
+ changeFrequency: "daily",
10447
+ priority: 0.7
10448
+ });
10449
+ }
10450
+ }
10451
+ return entries;
10452
+ }
10076
10453
  async function getCategorySitemapEntries(client, opts) {
10077
10454
  const base = opts.siteUrl.replace(/\/+$/, "");
10078
10455
  const basePath = opts.basePath ?? "/category";
@@ -10378,6 +10755,7 @@ function isCouponApplicableToProduct(coupon, productId) {
10378
10755
  getProductMetafieldsByType,
10379
10756
  getProductPrice,
10380
10757
  getProductPriceInfo,
10758
+ getProductSitemapEntries,
10381
10759
  getProductSwatches,
10382
10760
  getStockStatus,
10383
10761
  getVariantOptions,