@adobe/mysticat-shared-seo-client 1.5.0 → 1.6.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,16 @@
1
+ ## [@adobe/mysticat-shared-seo-client-v1.6.0](https://github.com/adobe/spacecat-shared/compare/@adobe/mysticat-shared-seo-client-v1.5.1...@adobe/mysticat-shared-seo-client-v1.6.0) (2026-06-08)
2
+
3
+ ### Features
4
+
5
+ * **seo-client:** add quality filters to broken-backlinks Semrush call (404+410, follow, text, no lostlinks) ([#1652](https://github.com/adobe/spacecat-shared/issues/1652)) ([875d92c](https://github.com/adobe/spacecat-shared/commit/875d92c4ceb306b5010118e29287bc81906e980e))
6
+
7
+ ## [@adobe/mysticat-shared-seo-client-v1.5.1](https://github.com/adobe/spacecat-shared/compare/@adobe/mysticat-shared-seo-client-v1.5.0...@adobe/mysticat-shared-seo-client-v1.5.1) (2026-05-29)
8
+
9
+ ### Bug Fixes
10
+
11
+ * **deps:** update adobe fixes ([#1609](https://github.com/adobe/spacecat-shared/issues/1609)) ([ccdcc2b](https://github.com/adobe/spacecat-shared/commit/ccdcc2b88c1b03e5c58ed682daaaadb382638395))
12
+ * **deps:** update external major (major) ([#1611](https://github.com/adobe/spacecat-shared/issues/1611)) ([5a80797](https://github.com/adobe/spacecat-shared/commit/5a807979187f1ebcc32b088faee9165aa38cc461))
13
+
1
14
  ## [@adobe/mysticat-shared-seo-client-v1.5.0](https://github.com/adobe/spacecat-shared/compare/@adobe/mysticat-shared-seo-client-v1.4.0...@adobe/mysticat-shared-seo-client-v1.5.0) (2026-05-27)
2
15
 
3
16
  ### Features
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adobe/mysticat-shared-seo-client",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Shared modules of the SpaceCat Services - SEO Client",
5
5
  "type": "module",
6
6
  "engines": {
@@ -35,7 +35,7 @@
35
35
  },
36
36
  "dependencies": {
37
37
  "@adobe/fetch": "4.3.0",
38
- "@adobe/helix-universal": "5.4.1",
38
+ "@adobe/helix-universal": "5.4.2",
39
39
  "@adobe/spacecat-shared-utils": "1.81.1",
40
40
  "urijs": "1.19.11"
41
41
  },
@@ -43,7 +43,7 @@
43
43
  "chai": "6.2.2",
44
44
  "chai-as-promised": "8.0.2",
45
45
  "nock": "14.0.15",
46
- "sinon": "21.1.2",
46
+ "sinon": "22.0.0",
47
47
  "sinon-chai": "4.0.1",
48
48
  "typescript": "6.0.3"
49
49
  }
package/src/client.js CHANGED
@@ -744,25 +744,49 @@ export default class SeoClient {
744
744
  const epLinks = ENDPOINTS.brokenBacklinks;
745
745
  const effectiveLimit = getLimit(limit, 100);
746
746
 
747
- // Step 1: Find broken (404) target pages, sorted by referring domains
748
- const { body: pagesBody, fullAuditRef } = await this.sendRawRequest({
747
+ // Step 1: Find broken target pages 404 and 410 in parallel (Semrush does not support OR
748
+ // within a single display_filter, so we issue two requests and merge the results).
749
+ const pageParams = (code) => ({
749
750
  type: epPages.type,
750
751
  target: url,
751
752
  target_type: 'domain',
752
753
  export_columns: epPages.columns,
753
754
  display_limit: effectiveLimit,
754
755
  display_filter: buildFilter([{
755
- sign: '+', field: 'responsecode', op: 'Eq', value: '404',
756
+ sign: '+', field: 'responsecode', op: 'Eq', value: code,
756
757
  }]),
757
758
  ...epPages.defaultParams,
758
- }, epPages.path);
759
- const brokenPages = parseCsvResponse(pagesBody);
759
+ });
760
+
761
+ const [{ body: pages404Body, fullAuditRef }, { body: pages410Body }] = await Promise.all([
762
+ this.sendRawRequest(pageParams('404'), epPages.path),
763
+ this.sendRawRequest(pageParams('410'), epPages.path),
764
+ ]);
765
+
766
+ // Merge, deduplicate by source_url, re-sort by domains_num descending, cap at limit.
767
+ const seenUrls = new Set();
768
+ const brokenPages = [
769
+ ...parseCsvResponse(pages404Body),
770
+ ...parseCsvResponse(pages410Body),
771
+ ]
772
+ .filter((p) => {
773
+ if (seenUrls.has(p.source_url)) {
774
+ return false;
775
+ }
776
+ seenUrls.add(p.source_url);
777
+ return true;
778
+ })
779
+ .sort((a, b) => Number(b.domains_num) - Number(a.domains_num))
780
+ .slice(0, effectiveLimit);
760
781
 
761
782
  if (brokenPages.length === 0) {
762
783
  return { result: { backlinks: [] }, fullAuditRef };
763
784
  }
764
785
 
765
- // Step 2: For each broken page, fetch the top backlink by authority score.
786
+ // Step 2: For each broken page, fetch the top backlink by authority score with quality filters:
787
+ // - follow links only (nofollow links carry no SEO value)
788
+ // - text links only (more actionable than image/frame links)
789
+ // - exclude lostlinks (only active broken backlinks, not already-removed ones)
766
790
  const brokenUrls = brokenPages.map((p) => p.source_url);
767
791
  const linkResults = await this.fanOut(brokenUrls, (brokenUrl) => this.sendRawRequest({
768
792
  type: epLinks.type,
@@ -770,6 +794,17 @@ export default class SeoClient {
770
794
  target_type: 'url',
771
795
  export_columns: epLinks.columns,
772
796
  display_limit: 1,
797
+ display_filter: buildFilter([
798
+ {
799
+ sign: '+', field: 'type', op: '', value: 'follow',
800
+ },
801
+ {
802
+ sign: '+', field: 'type', op: '', value: 'text',
803
+ },
804
+ {
805
+ sign: '-', field: 'type', op: '', value: 'lostlink',
806
+ },
807
+ ]),
773
808
  ...epLinks.defaultParams,
774
809
  }, epLinks.path), 'getBrokenBacklinks');
775
810