@adobe/mysticat-shared-seo-client 1.2.2 → 1.3.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/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## [@adobe/mysticat-shared-seo-client-v1.3.0](https://github.com/adobe/spacecat-shared/compare/@adobe/mysticat-shared-seo-client-v1.2.3...@adobe/mysticat-shared-seo-client-v1.3.0) (2026-04-28)
2
+
3
+ ### Features
4
+
5
+ * **seo-client:** return keywords[] array from getPaidPages() ([#1564](https://github.com/adobe/spacecat-shared/issues/1564)) ([71be968](https://github.com/adobe/spacecat-shared/commit/71be968ac133fc3c0d547d70e9663fcf2fe90437))
6
+
7
+ ## [@adobe/mysticat-shared-seo-client-v1.2.3](https://github.com/adobe/spacecat-shared/compare/@adobe/mysticat-shared-seo-client-v1.2.2...@adobe/mysticat-shared-seo-client-v1.2.3) (2026-04-15)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **seo-client:** omit display_date to avoid 5x historical pricing ([#1537](https://github.com/adobe/spacecat-shared/issues/1537)) ([4ffc683](https://github.com/adobe/spacecat-shared/commit/4ffc683db492d10414c2bc5c23804ec42d88b2c5))
12
+
1
13
  ## [@adobe/mysticat-shared-seo-client-v1.2.2](https://github.com/adobe/spacecat-shared/compare/@adobe/mysticat-shared-seo-client-v1.2.1...@adobe/mysticat-shared-seo-client-v1.2.2) (2026-04-14)
2
14
 
3
15
  ### Bug Fixes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/mysticat-shared-seo-client",
3
- "version": "1.2.2",
3
+ "version": "1.3.0",
4
4
  "description": "Shared modules of the SpaceCat Services - SEO Client",
5
5
  "type": "module",
6
6
  "engines": {
package/src/client.js CHANGED
@@ -15,7 +15,7 @@ import { context as h2, h1 } from '@adobe/fetch';
15
15
 
16
16
  import { ENDPOINTS } from './endpoints.js';
17
17
  import {
18
- parseCsvResponse, coerceValue, getLimit, toApiDate, fromApiDate, lastMonthISO, buildFilter,
18
+ parseCsvResponse, coerceValue, getLimit, toApiDate, fromApiDate, buildFilter,
19
19
  extractBrand, INTENT_CODES,
20
20
  } from './utils.js';
21
21
 
@@ -340,11 +340,28 @@ export default class SeoClient {
340
340
  };
341
341
  }
342
342
 
343
+ /**
344
+ * Fetch paid (PPC) pages and their keyword data from Semrush.
345
+ *
346
+ * Per-keyword CPC is returned as float dollars (e.g., 3.95) with null for unknown.
347
+ * This deliberately differs from getOrganicKeywords() which returns CPC as integer
348
+ * cents (e.g., 395) with 0 for unknown. The float-dollars convention was chosen for
349
+ * the keywords[] array because downstream consumers (import-worker, Mystique) expect
350
+ * dollars and the per-keyword CPC should not go through the cents round-trip used by
351
+ * the aggregate `value` field.
352
+ *
353
+ * @param {string} url - Domain to query
354
+ * @param {Object} [opts] - Options
355
+ * @param {string} [opts.date] - Report date
356
+ * @param {number} [opts.limit=200] - Max pages to return
357
+ * @param {string} [opts.region] - Limit to specific region
358
+ * @returns {Promise<{result: {pages: Array}, fullAuditRef: string}>}
359
+ */
343
360
  async getPaidPages(url, opts = {}) {
344
361
  if (typeof opts !== 'object' || opts === null) {
345
362
  throw new Error('Second argument must be an options object, not a positional value');
346
363
  }
347
- const { date = lastMonthISO(), limit = 200, region } = opts;
364
+ const { date, limit = 200, region } = opts;
348
365
  if (!hasText(url)) {
349
366
  throw new Error(`Invalid URL: ${url}`);
350
367
  }
@@ -358,15 +375,23 @@ export default class SeoClient {
358
375
  // Capped at MAX_PAID_KEYWORDS_FETCH to bound cost.
359
376
  const fetchLimit = Math.min(effectiveLimit * 10, MAX_PAID_KEYWORDS_FETCH);
360
377
 
361
- const dbResults = await this.fanOut(databases, (db) => this.sendRawRequest({
362
- type: ep.type,
363
- domain: url,
364
- database: db,
365
- display_date: toApiDate(date),
366
- display_limit: fetchLimit,
367
- export_columns: ep.columns,
368
- ...ep.defaultParams,
369
- }, ep.path), 'getPaidPages');
378
+ const dbResults = await this.fanOut(databases, (db) => {
379
+ const params = {
380
+ type: ep.type,
381
+ domain: url,
382
+ database: db,
383
+ display_limit: fetchLimit,
384
+ export_columns: ep.columns,
385
+ ...ep.defaultParams,
386
+ };
387
+ // Omitting display_date returns the provider's latest snapshot at live pricing
388
+ // (5x cheaper). The returned data may differ slightly from a pinned monthly
389
+ // snapshot, which is acceptable for our use case.
390
+ if (date) {
391
+ params.display_date = toApiDate(date);
392
+ }
393
+ return this.sendRawRequest(params, ep.path);
394
+ }, 'getPaidPages');
370
395
 
371
396
  // Group keywords by URL across all databases
372
397
  const pageMap = new Map();
@@ -412,6 +437,15 @@ export default class SeoClient {
412
437
  ),
413
438
  0,
414
439
  ),
440
+ keywords: page.keywords.map((kw) => ({
441
+ keyword: kw.Ph,
442
+ traffic: kw.kwTraffic,
443
+ cpc: coerceValue(kw.Cp, 'float') ?? null,
444
+ serp_title: kw.Tt || null,
445
+ position: coerceValue(kw.Po, 'int') ?? null,
446
+ volume: coerceValue(kw.Nq, 'int') ?? null,
447
+ country: kw.db.toUpperCase(),
448
+ })),
415
449
  };
416
450
  });
417
451
 
@@ -425,7 +459,7 @@ export default class SeoClient {
425
459
  if (typeof opts !== 'object' || opts === null) {
426
460
  throw new Error('Second argument must be an options object, not a positional value');
427
461
  }
428
- const { date = lastMonthISO(), region } = opts;
462
+ const { date, region } = opts;
429
463
  if (!hasText(url)) {
430
464
  throw new Error(`Invalid URL: ${url}`);
431
465
  }
@@ -433,14 +467,22 @@ export default class SeoClient {
433
467
  const ep = ENDPOINTS.metrics;
434
468
  const databases = getDatabases(region);
435
469
 
436
- const dbResults = await this.fanOut(databases, (db) => this.sendRawRequest({
437
- type: ep.type,
438
- domain: url,
439
- database: db,
440
- display_date: toApiDate(date),
441
- export_columns: ep.columns,
442
- ...ep.defaultParams,
443
- }, ep.path), 'getMetrics');
470
+ const dbResults = await this.fanOut(databases, (db) => {
471
+ const params = {
472
+ type: ep.type,
473
+ domain: url,
474
+ database: db,
475
+ export_columns: ep.columns,
476
+ ...ep.defaultParams,
477
+ };
478
+ // Omitting display_date returns the provider's latest snapshot at live pricing
479
+ // (5x cheaper). The returned data may differ slightly from a pinned monthly
480
+ // snapshot, which is acceptable for our use case.
481
+ if (date) {
482
+ params.display_date = toApiDate(date);
483
+ }
484
+ return this.sendRawRequest(params, ep.path);
485
+ }, 'getMetrics');
444
486
 
445
487
  const metrics = {
446
488
  org_keywords: 0,
@@ -758,7 +800,7 @@ export default class SeoClient {
758
800
  }
759
801
 
760
802
  // eslint-disable-next-line no-unused-vars, class-methods-use-this
761
- async getMetricsByCountry(url, date = lastMonthISO()) {
803
+ async getMetricsByCountry(url, date) {
762
804
  return STUB_RESPONSE;
763
805
  }
764
806
  }