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/README.md +6318 -6194
- package/dist/bot/bootstrap.global.js +12 -12
- package/dist/bot/index.js +10 -2
- package/dist/bot/index.mjs +10 -2
- package/dist/index.d.mts +324 -2
- package/dist/index.d.ts +324 -2
- package/dist/index.js +387 -9
- package/dist/index.mjs +386 -9
- package/package.json +2 -1
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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,
|