@adobe/spacecat-shared-utils 1.106.1 → 1.107.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,9 @@
|
|
|
1
|
+
## [@adobe/spacecat-shared-utils-v1.107.0](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.106.1...@adobe/spacecat-shared-utils-v1.107.0) (2026-03-30)
|
|
2
|
+
|
|
3
|
+
### Features
|
|
4
|
+
|
|
5
|
+
* add onboard config and opportunity maps ([#1464](https://github.com/adobe/spacecat-shared/issues/1464)) ([9bd59f5](https://github.com/adobe/spacecat-shared/commit/9bd59f5e2b05f3fd29ea7568daf0e369bd4d2535))
|
|
6
|
+
|
|
1
7
|
## [@adobe/spacecat-shared-utils-v1.106.1](https://github.com/adobe/spacecat-shared/compare/@adobe/spacecat-shared-utils-v1.106.0...@adobe/spacecat-shared-utils-v1.106.1) (2026-03-28)
|
|
2
8
|
|
|
3
9
|
### Bug Fixes
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -141,3 +141,25 @@ export {
|
|
|
141
141
|
getTokenTypeForOpportunity,
|
|
142
142
|
getCurrentCycle,
|
|
143
143
|
} from './token-grant-config.js';
|
|
144
|
+
|
|
145
|
+
export {
|
|
146
|
+
AUDIT_OPPORTUNITY_MAP,
|
|
147
|
+
getOpportunitiesForAudit,
|
|
148
|
+
getAuditsForOpportunity,
|
|
149
|
+
getAllOpportunityTypes,
|
|
150
|
+
getAllAuditTypes,
|
|
151
|
+
} from './opportunity/audit-mapping.js';
|
|
152
|
+
|
|
153
|
+
export {
|
|
154
|
+
DEPENDENCY_SOURCES,
|
|
155
|
+
OPPORTUNITY_DEPENDENCY_MAP,
|
|
156
|
+
getDependenciesForOpportunity,
|
|
157
|
+
getOpportunitiesForSource,
|
|
158
|
+
} from './opportunity/dependency-mapping.js';
|
|
159
|
+
|
|
160
|
+
export {
|
|
161
|
+
OPPORTUNITY_TITLES,
|
|
162
|
+
getOpportunityTitle,
|
|
163
|
+
} from './opportunity/opportunity-titles.js';
|
|
164
|
+
|
|
165
|
+
export { computeAuditCompletion } from './opportunity/audit-completion.js';
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Computes which audit types are pending vs completed from already-fetched audit records.
|
|
15
|
+
*
|
|
16
|
+
* The onboardStartTime anchor is the same value written to onboardConfig.lastStartTime
|
|
17
|
+
* in the site config by the api-service and passed via SQS/step function message to the
|
|
18
|
+
* task-processor. Callers supply it from whichever source is available to them.
|
|
19
|
+
*
|
|
20
|
+
* An audit is pending if:
|
|
21
|
+
* - No DB record exists for it yet, OR
|
|
22
|
+
* - Its auditedAt timestamp predates or ties with onboardStartTime (<=).
|
|
23
|
+
* An exact match indicates an in-flight audit captured before the run completed.
|
|
24
|
+
*
|
|
25
|
+
* If onboardStartTime is absent (site onboarded before this feature), any existing
|
|
26
|
+
* record is treated as completed — only a missing record counts as pending.
|
|
27
|
+
*
|
|
28
|
+
* Pure function — no DB calls.
|
|
29
|
+
*
|
|
30
|
+
* @param {string[]} auditTypes - Audit types to check
|
|
31
|
+
* @param {number|undefined} onboardStartTime - Onboard start timestamp in ms
|
|
32
|
+
* @param {Array} latestAudits - Already-fetched audit records (getAuditType, getAuditedAt)
|
|
33
|
+
* @returns {{ pendingAuditTypes: string[], completedAuditTypes: string[] }}
|
|
34
|
+
*/
|
|
35
|
+
export function computeAuditCompletion(auditTypes, onboardStartTime, latestAudits) {
|
|
36
|
+
const pendingAuditTypes = [];
|
|
37
|
+
const completedAuditTypes = [];
|
|
38
|
+
const auditsByType = {};
|
|
39
|
+
|
|
40
|
+
if (latestAudits) {
|
|
41
|
+
for (const audit of latestAudits) {
|
|
42
|
+
auditsByType[audit.getAuditType()] = audit;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
for (const auditType of auditTypes) {
|
|
47
|
+
const audit = auditsByType[auditType];
|
|
48
|
+
if (!audit) {
|
|
49
|
+
pendingAuditTypes.push(auditType);
|
|
50
|
+
} else if (onboardStartTime) {
|
|
51
|
+
const auditedAt = new Date(audit.getAuditedAt()).getTime();
|
|
52
|
+
// NaN or timestamp predating/matching trigger → pending
|
|
53
|
+
if (Number.isNaN(auditedAt) || auditedAt <= onboardStartTime) {
|
|
54
|
+
pendingAuditTypes.push(auditType);
|
|
55
|
+
} else {
|
|
56
|
+
completedAuditTypes.push(auditType);
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
// No onboard start time — treat existing record as completed.
|
|
60
|
+
completedAuditTypes.push(auditType);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return { pendingAuditTypes, completedAuditTypes };
|
|
65
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Maps each audit type to the opportunity types it can generate.
|
|
15
|
+
* Derived from the HANDLERS registry in spacecat-audit-worker/src/index.js.
|
|
16
|
+
*
|
|
17
|
+
* Audits that are data-collection only (e.g. apex, paid, lhs-mobile, cdn-logs-*)
|
|
18
|
+
* and produce no opportunities are intentionally omitted.
|
|
19
|
+
*
|
|
20
|
+
* Key: audit type string
|
|
21
|
+
* Value: array of opportunity type strings produced by that audit
|
|
22
|
+
*/
|
|
23
|
+
export const AUDIT_OPPORTUNITY_MAP = {
|
|
24
|
+
// Core SEO
|
|
25
|
+
'broken-backlinks': ['broken-backlinks'],
|
|
26
|
+
'broken-internal-links': ['broken-internal-links'],
|
|
27
|
+
canonical: ['canonical'],
|
|
28
|
+
hreflang: ['hreflang'],
|
|
29
|
+
'meta-tags': ['meta-tags'],
|
|
30
|
+
sitemap: ['sitemap'],
|
|
31
|
+
'sitemap-product-coverage': ['sitemap-product-coverage'],
|
|
32
|
+
'structured-data': ['structured-data'],
|
|
33
|
+
'redirect-chains': ['redirect-chains'],
|
|
34
|
+
|
|
35
|
+
// Performance
|
|
36
|
+
cwv: ['cwv'],
|
|
37
|
+
prerender: ['prerender'],
|
|
38
|
+
|
|
39
|
+
// Accessibility & content
|
|
40
|
+
accessibility: ['accessibility'],
|
|
41
|
+
'alt-text': ['alt-text'],
|
|
42
|
+
headings: ['headings'],
|
|
43
|
+
'no-cta-above-the-fold': ['no-cta-above-the-fold'],
|
|
44
|
+
readability: ['readability'],
|
|
45
|
+
|
|
46
|
+
// Forms — one audit, multiple opportunity types
|
|
47
|
+
'forms-opportunities': [
|
|
48
|
+
'high-form-views-low-conversions',
|
|
49
|
+
'high-page-views-low-form-nav',
|
|
50
|
+
'high-page-views-low-form-views',
|
|
51
|
+
'form-accessibility',
|
|
52
|
+
],
|
|
53
|
+
|
|
54
|
+
// Experimentation
|
|
55
|
+
'experimentation-opportunities': ['high-organic-low-ctr'],
|
|
56
|
+
|
|
57
|
+
// Commerce
|
|
58
|
+
'product-metatags': ['product-metatags'],
|
|
59
|
+
|
|
60
|
+
// Security
|
|
61
|
+
'security-csp': ['security-csp'],
|
|
62
|
+
'security-vulnerabilities': ['security-vulnerabilities'],
|
|
63
|
+
'security-permissions': ['security-permissions'],
|
|
64
|
+
'security-permissions-redundant': ['security-permissions-redundant'],
|
|
65
|
+
|
|
66
|
+
// LLMO / content quality
|
|
67
|
+
'llm-blocked': ['llm-blocked'],
|
|
68
|
+
'llm-error-pages': ['llm-error-pages'],
|
|
69
|
+
faqs: ['faqs'],
|
|
70
|
+
'related-urls': ['related-urls'],
|
|
71
|
+
toc: ['toc'],
|
|
72
|
+
|
|
73
|
+
// Experimentation (ESS signals)
|
|
74
|
+
'experimentation-ess-daily': ['experimentation-ess-daily'],
|
|
75
|
+
'experimentation-ess-monthly': ['experimentation-ess-monthly'],
|
|
76
|
+
|
|
77
|
+
// Offsite / brand analysis
|
|
78
|
+
'cited-analysis': ['cited-analysis'],
|
|
79
|
+
'wikipedia-analysis': ['wikipedia-analysis'],
|
|
80
|
+
'reddit-analysis': ['reddit-analysis'],
|
|
81
|
+
'youtube-analysis': ['youtube-analysis'],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
// ─── Query helpers ───────────────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns all opportunity types that a given audit type can generate.
|
|
88
|
+
* @param {string} auditType
|
|
89
|
+
* @returns {string[]}
|
|
90
|
+
*/
|
|
91
|
+
export function getOpportunitiesForAudit(auditType) {
|
|
92
|
+
return AUDIT_OPPORTUNITY_MAP[auditType] || [];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Returns all audit types that produce a given opportunity type.
|
|
97
|
+
* @param {string} opportunityType
|
|
98
|
+
* @returns {string[]}
|
|
99
|
+
*/
|
|
100
|
+
export function getAuditsForOpportunity(opportunityType) {
|
|
101
|
+
return Object.entries(AUDIT_OPPORTUNITY_MAP)
|
|
102
|
+
.filter(([, opps]) => opps.includes(opportunityType))
|
|
103
|
+
.map(([auditType]) => auditType);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Returns all unique opportunity types across all audits.
|
|
108
|
+
* @returns {string[]}
|
|
109
|
+
*/
|
|
110
|
+
export function getAllOpportunityTypes() {
|
|
111
|
+
return [...new Set(Object.values(AUDIT_OPPORTUNITY_MAP).flat())];
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Returns all audit types defined in the map.
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
export function getAllAuditTypes() {
|
|
119
|
+
return Object.keys(AUDIT_OPPORTUNITY_MAP);
|
|
120
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Dependency sources used by opportunities.
|
|
15
|
+
* RUM — Real User Monitoring (Adobe RUM / helix-rum-js)
|
|
16
|
+
* AHREFSImport — Ahrefs import job (organic traffic, backlinks, keywords)
|
|
17
|
+
* GSC — Google Search Console import
|
|
18
|
+
* scraping — Page HTML scraping (spacecat-scraper / spacecat-content-scraper)
|
|
19
|
+
* ExternalAPI — Third-party API call at audit run-time (Wikipedia, Reddit, YouTube)
|
|
20
|
+
* CrUX — Chrome User Experience Report (field data from Google)
|
|
21
|
+
* PSI — PageSpeed Insights (lab data via Lighthouse API)
|
|
22
|
+
*/
|
|
23
|
+
export const DEPENDENCY_SOURCES = /** @type {const} */ ({
|
|
24
|
+
RUM: 'RUM',
|
|
25
|
+
AHREFS_IMPORT: 'AHREFSImport',
|
|
26
|
+
GSC: 'GSC',
|
|
27
|
+
SCRAPING: 'scraping',
|
|
28
|
+
EXTERNAL_API: 'ExternalAPI',
|
|
29
|
+
CRUX: 'CrUX',
|
|
30
|
+
PSI: 'PSI',
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Maps each opportunity type to the data sources it requires to be populated.
|
|
35
|
+
* A missing or empty import for any listed source means the opportunity
|
|
36
|
+
* cannot be generated or will have incomplete data.
|
|
37
|
+
*
|
|
38
|
+
* Key: opportunity type string
|
|
39
|
+
* Value: array of DEPENDENCY_SOURCES values
|
|
40
|
+
*/
|
|
41
|
+
export const OPPORTUNITY_DEPENDENCY_MAP = {
|
|
42
|
+
// Performance
|
|
43
|
+
cwv: [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.CRUX, DEPENDENCY_SOURCES.PSI],
|
|
44
|
+
prerender: [DEPENDENCY_SOURCES.SCRAPING],
|
|
45
|
+
|
|
46
|
+
// SEO — traffic-driven
|
|
47
|
+
'high-organic-low-ctr': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.GSC],
|
|
48
|
+
|
|
49
|
+
// SEO — link audits
|
|
50
|
+
'broken-backlinks': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
|
|
51
|
+
'broken-internal-links': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.SCRAPING],
|
|
52
|
+
|
|
53
|
+
// SEO — on-page / content
|
|
54
|
+
canonical: [DEPENDENCY_SOURCES.SCRAPING],
|
|
55
|
+
hreflang: [DEPENDENCY_SOURCES.SCRAPING],
|
|
56
|
+
'meta-tags': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
|
|
57
|
+
sitemap: [DEPENDENCY_SOURCES.SCRAPING],
|
|
58
|
+
'sitemap-product-coverage': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
|
|
59
|
+
'structured-data': [DEPENDENCY_SOURCES.SCRAPING],
|
|
60
|
+
'redirect-chains': [DEPENDENCY_SOURCES.SCRAPING],
|
|
61
|
+
headings: [DEPENDENCY_SOURCES.SCRAPING],
|
|
62
|
+
|
|
63
|
+
// Accessibility & content
|
|
64
|
+
accessibility: [DEPENDENCY_SOURCES.SCRAPING],
|
|
65
|
+
'alt-text': [DEPENDENCY_SOURCES.AHREFS_IMPORT, DEPENDENCY_SOURCES.SCRAPING],
|
|
66
|
+
'no-cta-above-the-fold': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.SCRAPING],
|
|
67
|
+
readability: [DEPENDENCY_SOURCES.SCRAPING],
|
|
68
|
+
|
|
69
|
+
// Forms
|
|
70
|
+
'high-form-views-low-conversions': [DEPENDENCY_SOURCES.RUM],
|
|
71
|
+
'high-page-views-low-form-nav': [DEPENDENCY_SOURCES.RUM],
|
|
72
|
+
'high-page-views-low-form-views': [DEPENDENCY_SOURCES.RUM],
|
|
73
|
+
'form-accessibility': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.SCRAPING],
|
|
74
|
+
|
|
75
|
+
// Commerce
|
|
76
|
+
'product-metatags': [DEPENDENCY_SOURCES.SCRAPING],
|
|
77
|
+
|
|
78
|
+
// Security
|
|
79
|
+
'security-csp': [DEPENDENCY_SOURCES.SCRAPING],
|
|
80
|
+
'security-vulnerabilities': [DEPENDENCY_SOURCES.SCRAPING],
|
|
81
|
+
'security-permissions': [DEPENDENCY_SOURCES.SCRAPING],
|
|
82
|
+
'security-permissions-redundant': [DEPENDENCY_SOURCES.SCRAPING],
|
|
83
|
+
|
|
84
|
+
// LLMO / content quality
|
|
85
|
+
'llm-blocked': [DEPENDENCY_SOURCES.SCRAPING],
|
|
86
|
+
'llm-error-pages': [DEPENDENCY_SOURCES.SCRAPING],
|
|
87
|
+
faqs: [DEPENDENCY_SOURCES.SCRAPING],
|
|
88
|
+
'related-urls': [DEPENDENCY_SOURCES.AHREFS_IMPORT],
|
|
89
|
+
toc: [DEPENDENCY_SOURCES.SCRAPING],
|
|
90
|
+
|
|
91
|
+
// Experimentation (ESS signals — organic traffic + RUM)
|
|
92
|
+
'experimentation-ess-daily': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.GSC],
|
|
93
|
+
'experimentation-ess-monthly': [DEPENDENCY_SOURCES.RUM, DEPENDENCY_SOURCES.GSC],
|
|
94
|
+
|
|
95
|
+
// Offsite / brand analysis
|
|
96
|
+
'cited-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
|
|
97
|
+
'wikipedia-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
|
|
98
|
+
'reddit-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
|
|
99
|
+
'youtube-analysis': [DEPENDENCY_SOURCES.EXTERNAL_API],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// ─── Query helpers ───────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns the required data sources for a given opportunity type.
|
|
106
|
+
* @param {string} opportunityType
|
|
107
|
+
* @returns {string[]}
|
|
108
|
+
*/
|
|
109
|
+
export function getDependenciesForOpportunity(opportunityType) {
|
|
110
|
+
return OPPORTUNITY_DEPENDENCY_MAP[opportunityType] || [];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns all opportunity types that depend on a given data source.
|
|
115
|
+
* @param {string} source - One of DEPENDENCY_SOURCES values
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
export function getOpportunitiesForSource(source) {
|
|
119
|
+
return Object.entries(OPPORTUNITY_DEPENDENCY_MAP)
|
|
120
|
+
.filter(([, deps]) => deps.includes(source))
|
|
121
|
+
.map(([opportunityType]) => opportunityType);
|
|
122
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2025 Adobe. All rights reserved.
|
|
3
|
+
* This file is licensed to you under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
* you may not use this file except in compliance with the License. You may obtain a copy
|
|
5
|
+
* of the License at http://www.apache.org/licenses/LICENSE-2.0
|
|
6
|
+
*
|
|
7
|
+
* Unless required by applicable law or agreed to in writing, software distributed under
|
|
8
|
+
* the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR REPRESENTATIONS
|
|
9
|
+
* OF ANY KIND, either express or implied. See the License for the specific language
|
|
10
|
+
* governing permissions and limitations under the License.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Human-readable display titles for each opportunity type.
|
|
15
|
+
*
|
|
16
|
+
* Key: opportunity type string
|
|
17
|
+
* Value: display title string
|
|
18
|
+
*/
|
|
19
|
+
export const OPPORTUNITY_TITLES = {
|
|
20
|
+
// Performance
|
|
21
|
+
cwv: 'Core Web Vitals',
|
|
22
|
+
prerender: 'Prerender',
|
|
23
|
+
|
|
24
|
+
// Core SEO
|
|
25
|
+
'broken-backlinks': 'Broken Backlinks',
|
|
26
|
+
'broken-internal-links': 'Broken Internal Links',
|
|
27
|
+
canonical: 'Canonical',
|
|
28
|
+
hreflang: 'Hreflang',
|
|
29
|
+
'meta-tags': 'SEO Meta Tags',
|
|
30
|
+
sitemap: 'Sitemap',
|
|
31
|
+
'sitemap-product-coverage': 'Sitemap Product Coverage',
|
|
32
|
+
'structured-data': 'Structured Data',
|
|
33
|
+
'redirect-chains': 'Redirect Chains',
|
|
34
|
+
|
|
35
|
+
// Accessibility & content
|
|
36
|
+
accessibility: 'Accessibility',
|
|
37
|
+
'alt-text': 'Alt Text',
|
|
38
|
+
headings: 'Headings',
|
|
39
|
+
'no-cta-above-the-fold': 'No CTA Above the Fold',
|
|
40
|
+
readability: 'Readability',
|
|
41
|
+
|
|
42
|
+
// Forms
|
|
43
|
+
'high-form-views-low-conversions': 'High Form Views Low Conversions',
|
|
44
|
+
'high-page-views-low-form-nav': 'High Page Views Low Form Navigation',
|
|
45
|
+
'high-page-views-low-form-views': 'High Page Views Low Form Views',
|
|
46
|
+
'form-accessibility': 'Form Accessibility',
|
|
47
|
+
|
|
48
|
+
// Experimentation
|
|
49
|
+
'high-organic-low-ctr': 'High Organic Low CTR',
|
|
50
|
+
|
|
51
|
+
// Commerce
|
|
52
|
+
'product-metatags': 'Product Meta Tags',
|
|
53
|
+
|
|
54
|
+
// Security
|
|
55
|
+
'security-csp': 'Security CSP',
|
|
56
|
+
'security-vulnerabilities': 'Security Vulnerabilities',
|
|
57
|
+
'security-permissions': 'Security Permissions',
|
|
58
|
+
'security-permissions-redundant': 'Security Permissions Redundant',
|
|
59
|
+
|
|
60
|
+
// LLMO / content quality
|
|
61
|
+
'llm-blocked': 'LLM Blocked',
|
|
62
|
+
'llm-error-pages': 'LLM Error Pages',
|
|
63
|
+
faqs: 'FAQs',
|
|
64
|
+
'related-urls': 'Related URLs',
|
|
65
|
+
toc: 'Table of Contents',
|
|
66
|
+
|
|
67
|
+
// Experimentation (ESS signals)
|
|
68
|
+
'experimentation-ess-daily': 'Experimentation ESS Daily',
|
|
69
|
+
'experimentation-ess-monthly': 'Experimentation ESS Monthly',
|
|
70
|
+
|
|
71
|
+
// Offsite / brand analysis
|
|
72
|
+
'cited-analysis': 'Cited Analysis',
|
|
73
|
+
'wikipedia-analysis': 'Wikipedia Analysis',
|
|
74
|
+
'reddit-analysis': 'Reddit Analysis',
|
|
75
|
+
'youtube-analysis': 'YouTube Analysis',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Returns the human-readable title for a given opportunity type.
|
|
80
|
+
* Falls back to converting kebab-case to Title Case for unknown types.
|
|
81
|
+
* @param {string} opportunityType
|
|
82
|
+
* @returns {string}
|
|
83
|
+
*/
|
|
84
|
+
export function getOpportunityTitle(opportunityType) {
|
|
85
|
+
return OPPORTUNITY_TITLES[opportunityType]
|
|
86
|
+
?? opportunityType
|
|
87
|
+
.split('-')
|
|
88
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
89
|
+
.join(' ');
|
|
90
|
+
}
|