site-agent-pro 1.0.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 +689 -0
- package/dist/auth/credentialStore.js +62 -0
- package/dist/auth/inbox.js +193 -0
- package/dist/auth/profile.js +379 -0
- package/dist/auth/runner.js +1124 -0
- package/dist/backend/dashboardData.js +194 -0
- package/dist/backend/runArtifacts.js +48 -0
- package/dist/backend/runRepository.js +93 -0
- package/dist/bin.js +2 -0
- package/dist/cli/backfillSiteChecks.js +143 -0
- package/dist/cli/run.js +309 -0
- package/dist/cli/trade.js +69 -0
- package/dist/config.js +199 -0
- package/dist/core/agentProfiles.js +55 -0
- package/dist/core/aggregateReport.js +382 -0
- package/dist/core/audit.js +30 -0
- package/dist/core/customTaskSuite.js +148 -0
- package/dist/core/evaluator.js +217 -0
- package/dist/core/executor.js +788 -0
- package/dist/core/fallbackReport.js +335 -0
- package/dist/core/formHeuristics.js +411 -0
- package/dist/core/gameplaySummary.js +164 -0
- package/dist/core/interaction.js +202 -0
- package/dist/core/pageState.js +201 -0
- package/dist/core/planner.js +1669 -0
- package/dist/core/processSubmissionBatch.js +204 -0
- package/dist/core/runAuditJob.js +170 -0
- package/dist/core/runner.js +2352 -0
- package/dist/core/siteBrief.js +107 -0
- package/dist/core/siteChecks.js +1526 -0
- package/dist/core/taskDirectives.js +279 -0
- package/dist/core/taskHeuristics.js +263 -0
- package/dist/dashboard/client.js +1256 -0
- package/dist/dashboard/contracts.js +95 -0
- package/dist/dashboard/narrative.js +277 -0
- package/dist/dashboard/server.js +458 -0
- package/dist/dashboard/theme.js +888 -0
- package/dist/index.js +84 -0
- package/dist/llm/client.js +188 -0
- package/dist/paystack/account.js +123 -0
- package/dist/paystack/client.js +100 -0
- package/dist/paystack/index.js +13 -0
- package/dist/paystack/test-paystack.js +83 -0
- package/dist/paystack/transfer.js +138 -0
- package/dist/paystack/types.js +74 -0
- package/dist/paystack/webhook.js +121 -0
- package/dist/prompts/browserAgent.js +124 -0
- package/dist/prompts/reviewer.js +71 -0
- package/dist/reporting/clickReplay.js +290 -0
- package/dist/reporting/html.js +930 -0
- package/dist/reporting/markdown.js +238 -0
- package/dist/reporting/template.js +1141 -0
- package/dist/schemas/types.js +361 -0
- package/dist/submissions/customTasks.js +196 -0
- package/dist/submissions/html.js +770 -0
- package/dist/submissions/model.js +56 -0
- package/dist/submissions/publicUrl.js +76 -0
- package/dist/submissions/service.js +74 -0
- package/dist/submissions/store.js +37 -0
- package/dist/submissions/types.js +65 -0
- package/dist/trade/engine.js +241 -0
- package/dist/trade/evm/erc20.js +44 -0
- package/dist/trade/extractor.js +148 -0
- package/dist/trade/policy.js +35 -0
- package/dist/trade/session.js +31 -0
- package/dist/trade/types.js +107 -0
- package/dist/trade/validator.js +148 -0
- package/dist/utils/files.js +59 -0
- package/dist/utils/log.js +24 -0
- package/dist/utils/playwrightCompat.js +14 -0
- package/dist/utils/time.js +3 -0
- package/dist/wallet/provider.js +345 -0
- package/dist/wallet/relay.js +129 -0
- package/dist/wallet/wallet.js +178 -0
- package/docs/01-installation.md +134 -0
- package/docs/02-running-your-first-audit.md +136 -0
- package/docs/03-configuration.md +233 -0
- package/docs/04-how-the-agent-thinks.md +41 -0
- package/docs/05-extending-personas-and-tasks.md +42 -0
- package/docs/06-hardening-for-production.md +92 -0
- package/package.json +60 -0
|
@@ -0,0 +1,1141 @@
|
|
|
1
|
+
const IMAGE_URL_PATTERN = /\.(?:avif|gif|ico|jpe?g|png|svg|webp)(?:[?#]|$)/i;
|
|
2
|
+
const API_URL_PATTERN = /\/api(?:\/|$)|graphql|wp-json|\.json(?:[?#]|$)/i;
|
|
3
|
+
const SECURITY_PATTERN = /security|verification|captcha|cloudflare|access denied/i;
|
|
4
|
+
function normalizeText(value) {
|
|
5
|
+
return value.replace(/\s+/g, " ").trim();
|
|
6
|
+
}
|
|
7
|
+
function uniqueItems(items, limit) {
|
|
8
|
+
return [...new Set(items.map((item) => normalizeText(item)).filter(Boolean))].slice(0, limit);
|
|
9
|
+
}
|
|
10
|
+
function ensureSentence(value) {
|
|
11
|
+
const trimmed = normalizeText(value);
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
return "";
|
|
14
|
+
}
|
|
15
|
+
return /[.!?]$/.test(trimmed) ? trimmed : `${trimmed}.`;
|
|
16
|
+
}
|
|
17
|
+
function scoreToHundred(score) {
|
|
18
|
+
return Math.round(score * 10);
|
|
19
|
+
}
|
|
20
|
+
function formatScore(score) {
|
|
21
|
+
return `${scoreToHundred(score)}/100`;
|
|
22
|
+
}
|
|
23
|
+
function clampScore(value) {
|
|
24
|
+
return Math.min(10, Math.max(1, Math.round(value)));
|
|
25
|
+
}
|
|
26
|
+
function average(values) {
|
|
27
|
+
if (values.length === 0) {
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
return values.reduce((sum, value) => sum + value, 0) / values.length;
|
|
31
|
+
}
|
|
32
|
+
function labelFromScore(score) {
|
|
33
|
+
if (score >= 8) {
|
|
34
|
+
return "good";
|
|
35
|
+
}
|
|
36
|
+
if (score >= 6) {
|
|
37
|
+
return "warning";
|
|
38
|
+
}
|
|
39
|
+
return "poor";
|
|
40
|
+
}
|
|
41
|
+
function metricFromBoolean(label, value, verification, positiveLabel, negativeLabel) {
|
|
42
|
+
return {
|
|
43
|
+
label,
|
|
44
|
+
value: value ? positiveLabel : negativeLabel,
|
|
45
|
+
status: verification === "blocked" ? "blocked" : value ? "good" : "poor",
|
|
46
|
+
verification
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function metricFromCount(label, count, verification, options) {
|
|
50
|
+
return {
|
|
51
|
+
label,
|
|
52
|
+
value: `${count} ${count === 1 ? options.singular : options.plural}`,
|
|
53
|
+
status: verification === "blocked"
|
|
54
|
+
? "blocked"
|
|
55
|
+
: count <= options.goodAtMost
|
|
56
|
+
? "good"
|
|
57
|
+
: count <= options.warningAtMost
|
|
58
|
+
? "warning"
|
|
59
|
+
: "poor",
|
|
60
|
+
verification
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function metricFromMs(label, value, verification, thresholds, fallback) {
|
|
64
|
+
if (value === null || value <= 0) {
|
|
65
|
+
return {
|
|
66
|
+
label,
|
|
67
|
+
value: fallback,
|
|
68
|
+
status: verification === "blocked" ? "blocked" : "warning",
|
|
69
|
+
verification
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
label,
|
|
74
|
+
value: `${Math.round(value)}ms`,
|
|
75
|
+
status: verification === "blocked"
|
|
76
|
+
? "blocked"
|
|
77
|
+
: value <= thresholds.goodAtMost
|
|
78
|
+
? "good"
|
|
79
|
+
: value <= thresholds.warningAtMost
|
|
80
|
+
? "warning"
|
|
81
|
+
: "poor",
|
|
82
|
+
verification
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function metricFromRatio(label, value, verification, thresholds, digits, fallback) {
|
|
86
|
+
if (value === null) {
|
|
87
|
+
return {
|
|
88
|
+
label,
|
|
89
|
+
value: fallback,
|
|
90
|
+
status: verification === "blocked" ? "blocked" : "warning",
|
|
91
|
+
verification
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
label,
|
|
96
|
+
value: value.toFixed(digits),
|
|
97
|
+
status: verification === "blocked"
|
|
98
|
+
? "blocked"
|
|
99
|
+
: value <= thresholds.goodAtMost
|
|
100
|
+
? "good"
|
|
101
|
+
: value <= thresholds.warningAtMost
|
|
102
|
+
? "warning"
|
|
103
|
+
: "poor",
|
|
104
|
+
verification
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
function metricFromText(label, value, status, verification) {
|
|
108
|
+
return { label, value, status, verification };
|
|
109
|
+
}
|
|
110
|
+
function metricFromAffectedPages(label, affectedPages, totalPages, verification, thresholds, fallback) {
|
|
111
|
+
if (totalPages <= 0) {
|
|
112
|
+
return {
|
|
113
|
+
label,
|
|
114
|
+
value: fallback,
|
|
115
|
+
status: verification === "blocked" ? "blocked" : "warning",
|
|
116
|
+
verification
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
const percentage = (affectedPages / totalPages) * 100;
|
|
120
|
+
return {
|
|
121
|
+
label,
|
|
122
|
+
value: `${affectedPages}/${totalPages} pages`,
|
|
123
|
+
status: verification === "blocked"
|
|
124
|
+
? "blocked"
|
|
125
|
+
: percentage <= thresholds.goodAtMostPct
|
|
126
|
+
? "good"
|
|
127
|
+
: percentage <= thresholds.warningAtMostPct
|
|
128
|
+
? "warning"
|
|
129
|
+
: "poor",
|
|
130
|
+
verification
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export function labelForMetricStatus(status) {
|
|
134
|
+
switch (status) {
|
|
135
|
+
case "good":
|
|
136
|
+
return "Good";
|
|
137
|
+
case "warning":
|
|
138
|
+
return "Needs improvement";
|
|
139
|
+
case "poor":
|
|
140
|
+
return "Poor";
|
|
141
|
+
case "blocked":
|
|
142
|
+
return "Blocked";
|
|
143
|
+
default:
|
|
144
|
+
return "Needs improvement";
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
export function labelForCoverageStatus(status) {
|
|
148
|
+
switch (status) {
|
|
149
|
+
case "verified":
|
|
150
|
+
return "Verified";
|
|
151
|
+
case "inferred":
|
|
152
|
+
return "Inferred";
|
|
153
|
+
case "blocked":
|
|
154
|
+
return "Blocked";
|
|
155
|
+
default:
|
|
156
|
+
return "Inferred";
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function formatAuditDate(startedAt, timeZone) {
|
|
160
|
+
if (!startedAt) {
|
|
161
|
+
return "Unknown";
|
|
162
|
+
}
|
|
163
|
+
const parsed = new Date(startedAt);
|
|
164
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
165
|
+
return startedAt;
|
|
166
|
+
}
|
|
167
|
+
return parsed.toLocaleString(undefined, {
|
|
168
|
+
dateStyle: "medium",
|
|
169
|
+
timeStyle: "short",
|
|
170
|
+
...(timeZone ? { timeZone } : {})
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function isRecord(value) {
|
|
174
|
+
return Boolean(value) && typeof value === "object";
|
|
175
|
+
}
|
|
176
|
+
function coverageFrom(note, fallback) {
|
|
177
|
+
if (!note) {
|
|
178
|
+
return fallback;
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
status: note.status,
|
|
182
|
+
summary: ensureSentence(note.summary),
|
|
183
|
+
evidence: uniqueItems(note.evidence.map((item) => ensureSentence(item)), 5),
|
|
184
|
+
blockers: uniqueItems(note.blockers.map((item) => ensureSentence(item)), 4)
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function hasProbeDomEvidence(probe) {
|
|
188
|
+
if (!probe) {
|
|
189
|
+
return false;
|
|
190
|
+
}
|
|
191
|
+
return Boolean(probe.title ||
|
|
192
|
+
probe.metaDescription ||
|
|
193
|
+
probe.h1Count > 0 ||
|
|
194
|
+
probe.h2Count > 0 ||
|
|
195
|
+
probe.visibleLinkCount > 0 ||
|
|
196
|
+
probe.internalLinkSamples.length > 0 ||
|
|
197
|
+
probe.ctaSamples.length > 0 ||
|
|
198
|
+
probe.formCount > 0 ||
|
|
199
|
+
probe.wordCount > 0 ||
|
|
200
|
+
probe.mediaCount > 0 ||
|
|
201
|
+
probe.heroText);
|
|
202
|
+
}
|
|
203
|
+
function probeVerification(probe, fallback) {
|
|
204
|
+
if (!probe) {
|
|
205
|
+
return fallback;
|
|
206
|
+
}
|
|
207
|
+
return probe.loadOk || hasProbeDomEvidence(probe) ? "verified" : fallback;
|
|
208
|
+
}
|
|
209
|
+
function performanceVerification(probe, fallback) {
|
|
210
|
+
if (!probe) {
|
|
211
|
+
return fallback;
|
|
212
|
+
}
|
|
213
|
+
return probe.loadOk ||
|
|
214
|
+
probe.performance.domContentLoadedMs !== null ||
|
|
215
|
+
probe.performance.loadMs !== null ||
|
|
216
|
+
probe.performance.firstContentfulPaintMs !== null ||
|
|
217
|
+
probe.performance.largestContentfulPaintMs !== null ||
|
|
218
|
+
probe.performance.cumulativeLayoutShift !== null
|
|
219
|
+
? "verified"
|
|
220
|
+
: fallback;
|
|
221
|
+
}
|
|
222
|
+
function collectRawSignals(args) {
|
|
223
|
+
const visitedTitles = uniqueItems(args.taskResults.flatMap((task) => [
|
|
224
|
+
task.finalTitle,
|
|
225
|
+
...task.history.map((entry) => entry.title)
|
|
226
|
+
]), 10);
|
|
227
|
+
const signals = {
|
|
228
|
+
navigationErrors: 0,
|
|
229
|
+
requestFailures: 0,
|
|
230
|
+
imageFailures: 0,
|
|
231
|
+
apiFailures: 0,
|
|
232
|
+
consoleErrors: 0,
|
|
233
|
+
consoleWarnings: 0,
|
|
234
|
+
pageErrors: 0,
|
|
235
|
+
securityBarriers: 0,
|
|
236
|
+
failedInteractions: 0,
|
|
237
|
+
stalledInteractions: 0,
|
|
238
|
+
formInteractions: 0,
|
|
239
|
+
formFailures: 0,
|
|
240
|
+
visitedTitles
|
|
241
|
+
};
|
|
242
|
+
for (const event of args.rawEvents ?? []) {
|
|
243
|
+
if (!isRecord(event)) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
const type = typeof event.type === "string" ? event.type : "";
|
|
247
|
+
const level = typeof event.level === "string" ? event.level : "";
|
|
248
|
+
const note = typeof event.note === "string" ? event.note : "";
|
|
249
|
+
const text = typeof event.text === "string" ? event.text : "";
|
|
250
|
+
const url = typeof event.url === "string" ? event.url : "";
|
|
251
|
+
const combined = `${note} ${text} ${url}`;
|
|
252
|
+
if (type === "navigation_error") {
|
|
253
|
+
signals.navigationErrors += 1;
|
|
254
|
+
}
|
|
255
|
+
if (type === "requestfailed") {
|
|
256
|
+
signals.requestFailures += 1;
|
|
257
|
+
if (IMAGE_URL_PATTERN.test(url) || /\/images?\//i.test(url)) {
|
|
258
|
+
signals.imageFailures += 1;
|
|
259
|
+
}
|
|
260
|
+
if (API_URL_PATTERN.test(url)) {
|
|
261
|
+
signals.apiFailures += 1;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
if (type === "pageerror") {
|
|
265
|
+
signals.pageErrors += 1;
|
|
266
|
+
}
|
|
267
|
+
if (type === "console") {
|
|
268
|
+
if (/error/i.test(level)) {
|
|
269
|
+
signals.consoleErrors += 1;
|
|
270
|
+
}
|
|
271
|
+
else if (/warn/i.test(level)) {
|
|
272
|
+
signals.consoleWarnings += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (SECURITY_PATTERN.test(combined)) {
|
|
276
|
+
signals.securityBarriers += 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
for (const task of args.taskResults) {
|
|
280
|
+
for (const entry of task.history) {
|
|
281
|
+
const note = entry.result.note ?? "";
|
|
282
|
+
if (!entry.result.success) {
|
|
283
|
+
signals.failedInteractions += 1;
|
|
284
|
+
}
|
|
285
|
+
if (/timeout|no clear visible change|unchanged page states/i.test(note)) {
|
|
286
|
+
signals.stalledInteractions += 1;
|
|
287
|
+
}
|
|
288
|
+
if (entry.decision.action === "type") {
|
|
289
|
+
signals.formInteractions += 1;
|
|
290
|
+
if (!entry.result.success) {
|
|
291
|
+
signals.formFailures += 1;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (SECURITY_PATTERN.test(note)) {
|
|
295
|
+
signals.securityBarriers += 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return signals;
|
|
300
|
+
}
|
|
301
|
+
function buildFallbackSiteChecks(args) {
|
|
302
|
+
return {
|
|
303
|
+
generatedAt: new Date().toISOString(),
|
|
304
|
+
baseUrl: args.website,
|
|
305
|
+
finalResolvedUrl: null,
|
|
306
|
+
coverage: {
|
|
307
|
+
performance: { status: "blocked", summary: "Performance checks were not available for this run.", evidence: [], blockers: [] },
|
|
308
|
+
seo: { status: "blocked", summary: "SEO checks were not available for this run.", evidence: [], blockers: [] },
|
|
309
|
+
uiux: { status: "inferred", summary: "UI and UX coverage relies on task interaction evidence.", evidence: [], blockers: [] },
|
|
310
|
+
security: { status: "blocked", summary: "Security checks were not available for this run.", evidence: [], blockers: [] },
|
|
311
|
+
technicalHealth: { status: "inferred", summary: "Technical health relies on saved browser events.", evidence: [], blockers: [] },
|
|
312
|
+
mobileOptimization: { status: "blocked", summary: "Mobile checks were not available for this run.", evidence: [], blockers: [] },
|
|
313
|
+
contentQuality: { status: "blocked", summary: "Content checks were not available for this run.", evidence: [], blockers: [] },
|
|
314
|
+
cro: { status: "inferred", summary: "CRO relies on visible task outcomes in this run.", evidence: [], blockers: [] }
|
|
315
|
+
},
|
|
316
|
+
performance: {
|
|
317
|
+
desktop: null,
|
|
318
|
+
mobile: null,
|
|
319
|
+
failedRequestCount: args.signals.requestFailures,
|
|
320
|
+
imageFailureCount: args.signals.imageFailures,
|
|
321
|
+
apiFailureCount: args.signals.apiFailures,
|
|
322
|
+
navigationErrorCount: args.signals.navigationErrors,
|
|
323
|
+
stalledInteractionCount: args.signals.stalledInteractions,
|
|
324
|
+
evidence: []
|
|
325
|
+
},
|
|
326
|
+
seo: {
|
|
327
|
+
robotsTxt: { url: new URL("/robots.txt", args.website).toString(), ok: false, statusCode: null, note: "No SEO artifact was saved for this run." },
|
|
328
|
+
sitemap: { url: new URL("/sitemap.xml", args.website).toString(), ok: false, statusCode: null, note: "No SEO artifact was saved for this run." },
|
|
329
|
+
brokenLinkCount: 0,
|
|
330
|
+
checkedLinkCount: 0,
|
|
331
|
+
brokenLinks: [],
|
|
332
|
+
evidence: []
|
|
333
|
+
},
|
|
334
|
+
security: {
|
|
335
|
+
https: args.website.startsWith("https://"),
|
|
336
|
+
secureTransportVerified: false,
|
|
337
|
+
initialStatusCode: null,
|
|
338
|
+
securityHeaders: [],
|
|
339
|
+
missingHeaders: [],
|
|
340
|
+
evidence: []
|
|
341
|
+
},
|
|
342
|
+
technicalHealth: {
|
|
343
|
+
framework: null,
|
|
344
|
+
consoleErrorCount: args.signals.consoleErrors,
|
|
345
|
+
consoleWarningCount: args.signals.consoleWarnings,
|
|
346
|
+
pageErrorCount: args.signals.pageErrors,
|
|
347
|
+
apiFailureCount: args.signals.apiFailures,
|
|
348
|
+
evidence: []
|
|
349
|
+
},
|
|
350
|
+
mobileOptimization: {
|
|
351
|
+
desktop: null,
|
|
352
|
+
mobile: null,
|
|
353
|
+
responsiveVerdict: "blocked",
|
|
354
|
+
evidence: []
|
|
355
|
+
},
|
|
356
|
+
contentQuality: {
|
|
357
|
+
readabilityScore: null,
|
|
358
|
+
readabilityLabel: "Blocked",
|
|
359
|
+
wordCount: 0,
|
|
360
|
+
longParagraphCount: 0,
|
|
361
|
+
mediaCount: 0,
|
|
362
|
+
evidence: []
|
|
363
|
+
},
|
|
364
|
+
cro: {
|
|
365
|
+
ctaCount: 0,
|
|
366
|
+
primaryCtas: [],
|
|
367
|
+
formCount: 0,
|
|
368
|
+
submitControlCount: 0,
|
|
369
|
+
trustSignalCount: 0,
|
|
370
|
+
evidence: []
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
function buildBusinessImpact(report) {
|
|
375
|
+
if (report.scores.conversion_readiness <= 4) {
|
|
376
|
+
return "Current friction is likely to reduce trust in the primary conversion path and increase drop-off before visitors complete a high-value action.";
|
|
377
|
+
}
|
|
378
|
+
if (report.scores.navigation <= 4 || report.scores.clarity <= 4) {
|
|
379
|
+
return "Unclear navigation and weak first-click feedback are likely to slow discovery of important pages and suppress engagement.";
|
|
380
|
+
}
|
|
381
|
+
if (report.scores.trust <= 4) {
|
|
382
|
+
return "Trust gaps may hold visitors back from moving forward, even when the site appears technically usable.";
|
|
383
|
+
}
|
|
384
|
+
return "The experience is usable in parts, but sharper execution on the main path would make the site more reliable and conversion-ready.";
|
|
385
|
+
}
|
|
386
|
+
function scorePerformance(siteChecks, report) {
|
|
387
|
+
let score = average([report.scores.navigation, report.scores.friction]);
|
|
388
|
+
const desktop = siteChecks.performance.desktop;
|
|
389
|
+
if (desktop?.performance.largestContentfulPaintMs) {
|
|
390
|
+
score += desktop.performance.largestContentfulPaintMs <= 2500 ? 1 : desktop.performance.largestContentfulPaintMs <= 4000 ? 0 : -1;
|
|
391
|
+
}
|
|
392
|
+
if (desktop?.performance.cumulativeLayoutShift !== null && desktop?.performance.cumulativeLayoutShift !== undefined) {
|
|
393
|
+
score += desktop.performance.cumulativeLayoutShift <= 0.1 ? 1 : desktop.performance.cumulativeLayoutShift <= 0.25 ? 0 : -1;
|
|
394
|
+
}
|
|
395
|
+
score -= Math.min(2, siteChecks.performance.failedRequestCount * 0.2);
|
|
396
|
+
return clampScore(score);
|
|
397
|
+
}
|
|
398
|
+
function scoreSeo(siteChecks) {
|
|
399
|
+
const desktop = siteChecks.performance.desktop;
|
|
400
|
+
const crawlSummary = siteChecks.seo.crawlSummary;
|
|
401
|
+
const pageStats = siteChecks.seo.pageStats;
|
|
402
|
+
const auditedPages = crawlSummary?.totalPagesAudited ?? 0;
|
|
403
|
+
let score = 5;
|
|
404
|
+
if (desktop?.title) {
|
|
405
|
+
score += desktop.title.length >= 10 && desktop.title.length <= 65 ? 1 : 0;
|
|
406
|
+
}
|
|
407
|
+
if (desktop?.metaDescription) {
|
|
408
|
+
score += desktop.metaDescription.length >= 50 && desktop.metaDescription.length <= 160 ? 1 : 0;
|
|
409
|
+
}
|
|
410
|
+
if (desktop?.h1Count === 1) {
|
|
411
|
+
score += 1;
|
|
412
|
+
}
|
|
413
|
+
else if ((desktop?.h1Count ?? 0) === 0) {
|
|
414
|
+
score -= 1;
|
|
415
|
+
}
|
|
416
|
+
if (siteChecks.seo.robotsTxt.ok) {
|
|
417
|
+
score += 1;
|
|
418
|
+
}
|
|
419
|
+
if (siteChecks.seo.sitemap.ok) {
|
|
420
|
+
score += 1;
|
|
421
|
+
}
|
|
422
|
+
score -= Math.min(2, siteChecks.seo.brokenLinkCount);
|
|
423
|
+
if ((desktop?.structuredDataCount ?? 0) > 0) {
|
|
424
|
+
score += 1;
|
|
425
|
+
}
|
|
426
|
+
if (auditedPages > 0 && pageStats) {
|
|
427
|
+
const titleIssueRate = (pageStats.pagesMissingTitle + pageStats.pagesBadTitleLength) / auditedPages;
|
|
428
|
+
const metaIssueRate = (pageStats.pagesMissingMetaDescription + pageStats.pagesBadMetaDescriptionLength) / auditedPages;
|
|
429
|
+
const contentIssueRate = (pageStats.pagesMissingH1 +
|
|
430
|
+
pageStats.pagesWithMultipleH1 +
|
|
431
|
+
pageStats.pagesLowWordCount +
|
|
432
|
+
pageStats.pagesThinOrPlaceholder +
|
|
433
|
+
pageStats.pagesWithHeadingOrderIssues) /
|
|
434
|
+
auditedPages;
|
|
435
|
+
const metadataIssueRate = (pageStats.pagesMissingCanonical +
|
|
436
|
+
pageStats.pagesNonSelfCanonical +
|
|
437
|
+
pageStats.pagesMissingOpenGraphBasics +
|
|
438
|
+
pageStats.pagesMissingTwitterCard +
|
|
439
|
+
pageStats.pagesWithUrlIssues) /
|
|
440
|
+
auditedPages;
|
|
441
|
+
const technicalIssueRate = (pageStats.pagesMissingViewport +
|
|
442
|
+
pageStats.pagesMissingCharset +
|
|
443
|
+
pageStats.pagesMissingLang +
|
|
444
|
+
pageStats.pagesWithRenderBlockingHeadScripts +
|
|
445
|
+
pageStats.pagesWithNonLazyImages +
|
|
446
|
+
pageStats.pagesWithUnlabeledInputs +
|
|
447
|
+
pageStats.pagesWithUnlabeledInteractive +
|
|
448
|
+
pageStats.pagesMissingSkipNav) /
|
|
449
|
+
auditedPages;
|
|
450
|
+
score += 1;
|
|
451
|
+
score -= Math.min(1.5, titleIssueRate * 3);
|
|
452
|
+
score -= Math.min(1.5, metaIssueRate * 3);
|
|
453
|
+
score -= Math.min(1.5, contentIssueRate * 1.5);
|
|
454
|
+
score -= Math.min(1, metadataIssueRate * 1.5);
|
|
455
|
+
score -= Math.min(1, technicalIssueRate);
|
|
456
|
+
if (pageStats.pagesWithStructuredData > 0) {
|
|
457
|
+
score += 0.5;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
return clampScore(score);
|
|
461
|
+
}
|
|
462
|
+
function scoreSecurity(siteChecks) {
|
|
463
|
+
let score = siteChecks.security.https ? 6 : 2;
|
|
464
|
+
if (siteChecks.security.secureTransportVerified) {
|
|
465
|
+
score += 1;
|
|
466
|
+
}
|
|
467
|
+
const presentHeaders = siteChecks.security.securityHeaders.filter((header) => header.present).length;
|
|
468
|
+
score += presentHeaders / 2;
|
|
469
|
+
return clampScore(score);
|
|
470
|
+
}
|
|
471
|
+
function scoreMobile(siteChecks) {
|
|
472
|
+
const mobile = siteChecks.mobileOptimization.mobile;
|
|
473
|
+
if (!mobile || siteChecks.mobileOptimization.responsiveVerdict === "blocked") {
|
|
474
|
+
return 2;
|
|
475
|
+
}
|
|
476
|
+
let score = 7;
|
|
477
|
+
if (mobile.horizontalOverflow) {
|
|
478
|
+
score -= 3;
|
|
479
|
+
}
|
|
480
|
+
score -= Math.min(2, mobile.tapTargetIssueCount * 0.5);
|
|
481
|
+
score -= Math.min(2, mobile.smallTextIssueCount * 0.2);
|
|
482
|
+
if (siteChecks.mobileOptimization.responsiveVerdict === "mixed") {
|
|
483
|
+
score -= 1;
|
|
484
|
+
}
|
|
485
|
+
if (siteChecks.mobileOptimization.responsiveVerdict === "responsive") {
|
|
486
|
+
score += 1;
|
|
487
|
+
}
|
|
488
|
+
return clampScore(score);
|
|
489
|
+
}
|
|
490
|
+
function buildAccessibilityMetric(accessibility) {
|
|
491
|
+
if (!accessibility) {
|
|
492
|
+
return {
|
|
493
|
+
label: "Accessibility",
|
|
494
|
+
value: "Accessibility artifact was unavailable for this run",
|
|
495
|
+
status: "blocked",
|
|
496
|
+
verification: "blocked"
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
const severeIssues = accessibility.violations.filter((violation) => violation.impact === "serious" || violation.impact === "critical").length;
|
|
500
|
+
if (accessibility.error && accessibility.violations.length === 0) {
|
|
501
|
+
return {
|
|
502
|
+
label: "Accessibility",
|
|
503
|
+
value: accessibility.error,
|
|
504
|
+
status: "blocked",
|
|
505
|
+
verification: "blocked"
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
if (accessibility.violations.length === 0) {
|
|
509
|
+
return {
|
|
510
|
+
label: "Accessibility",
|
|
511
|
+
value: "No automated violations found in the saved scan",
|
|
512
|
+
status: "good",
|
|
513
|
+
verification: "verified"
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
return {
|
|
517
|
+
label: "Accessibility",
|
|
518
|
+
value: `${accessibility.violations.length} automated issue(s), including ${severeIssues} higher-impact finding(s)`,
|
|
519
|
+
status: severeIssues > 0 ? "poor" : "warning",
|
|
520
|
+
verification: accessibility.error ? "inferred" : "verified"
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
function buildActionPlan(args) {
|
|
524
|
+
const accessibilityFixes = (args.accessibility?.violations ?? [])
|
|
525
|
+
.slice(0, 2)
|
|
526
|
+
.map((violation) => `Fix the "${violation.id}" accessibility issue affecting ${violation.nodes} node(s).`);
|
|
527
|
+
const securityFixes = args.siteChecks.security.missingHeaders.length > 0
|
|
528
|
+
? [`Add the missing security headers: ${args.siteChecks.security.missingHeaders.join(", ")}.`]
|
|
529
|
+
: [];
|
|
530
|
+
const mobileFixes = args.siteChecks.mobileOptimization.responsiveVerdict === "poor" || args.siteChecks.mobileOptimization.responsiveVerdict === "mixed"
|
|
531
|
+
? args.siteChecks.mobileOptimization.evidence
|
|
532
|
+
.filter((item) => /overflow|tap target|small-text|text sizing/i.test(item))
|
|
533
|
+
.map((item) => `Resolve the mobile issue highlighted by the probe: ${item}`)
|
|
534
|
+
: [];
|
|
535
|
+
const high = uniqueItems([...args.report.top_fixes, ...mobileFixes, ...accessibilityFixes].map((item) => ensureSentence(item)), 3);
|
|
536
|
+
const medium = uniqueItems([...securityFixes, ...accessibilityFixes, ...args.siteChecks.seo.brokenLinks.map((item) => `Repair or redirect the failing sampled link: ${item.url}`)].map((item) => ensureSentence(item)), 3).filter((item) => !high.includes(item));
|
|
537
|
+
const low = uniqueItems([
|
|
538
|
+
args.siteChecks.performance.desktop?.performance.largestContentfulPaintMs && args.siteChecks.performance.desktop.performance.largestContentfulPaintMs > 2500
|
|
539
|
+
? "Optimize above-the-fold assets to bring the largest contentful paint down."
|
|
540
|
+
: "",
|
|
541
|
+
args.siteChecks.contentQuality.longParagraphCount > 0
|
|
542
|
+
? "Shorten long paragraphs and increase scanability in dense content blocks."
|
|
543
|
+
: "",
|
|
544
|
+
args.siteChecks.cro.trustSignalCount === 0
|
|
545
|
+
? "Add stronger trust signals near the main CTA and conversion path."
|
|
546
|
+
: ""
|
|
547
|
+
].map((item) => ensureSentence(item)), 3).filter((item) => !high.includes(item) && !medium.includes(item));
|
|
548
|
+
return { high, medium, low };
|
|
549
|
+
}
|
|
550
|
+
function buildConfidence(args) {
|
|
551
|
+
const allFailed = args.report.task_results.length > 0 && args.report.task_results.every((task) => task.status === "failed");
|
|
552
|
+
const blockedSections = Object.values(args.siteChecks.coverage).filter((coverage) => coverage.status === "blocked").length;
|
|
553
|
+
const limitedAccessibility = Boolean(args.accessibility?.error) && args.accessibility?.violations.length === 0;
|
|
554
|
+
const majorLimitations = [allFailed, limitedAccessibility, blockedSections >= 3, args.signals.securityBarriers > 0].filter(Boolean).length;
|
|
555
|
+
if (majorLimitations >= 2) {
|
|
556
|
+
return "Low";
|
|
557
|
+
}
|
|
558
|
+
if (majorLimitations === 0 && args.report.task_results.some((task) => task.status !== "failed")) {
|
|
559
|
+
return "High";
|
|
560
|
+
}
|
|
561
|
+
return "Medium";
|
|
562
|
+
}
|
|
563
|
+
export function buildStructuredReviewTemplate(args) {
|
|
564
|
+
const signals = collectRawSignals({
|
|
565
|
+
report: args.report,
|
|
566
|
+
taskResults: args.taskResults,
|
|
567
|
+
rawEvents: args.rawEvents
|
|
568
|
+
});
|
|
569
|
+
const siteChecks = args.siteChecks ?? buildFallbackSiteChecks({ website: args.website, report: args.report, signals });
|
|
570
|
+
const performanceScore = scorePerformance(siteChecks, args.report);
|
|
571
|
+
const seoScore = scoreSeo(siteChecks);
|
|
572
|
+
const uiuxScore = clampScore(average([args.report.scores.clarity, args.report.scores.navigation, args.report.scores.trust]));
|
|
573
|
+
const securityScore = scoreSecurity(siteChecks);
|
|
574
|
+
const mobileScore = scoreMobile(siteChecks);
|
|
575
|
+
const performanceCoverage = coverageFrom(siteChecks.coverage.performance, {
|
|
576
|
+
status: "blocked",
|
|
577
|
+
summary: "Performance checks were unavailable for this run.",
|
|
578
|
+
evidence: [],
|
|
579
|
+
blockers: []
|
|
580
|
+
});
|
|
581
|
+
const seoCoverage = coverageFrom(siteChecks.coverage.seo, {
|
|
582
|
+
status: "blocked",
|
|
583
|
+
summary: "SEO checks were unavailable for this run.",
|
|
584
|
+
evidence: [],
|
|
585
|
+
blockers: []
|
|
586
|
+
});
|
|
587
|
+
const uiuxCoverage = coverageFrom(siteChecks.coverage.uiux, {
|
|
588
|
+
status: "inferred",
|
|
589
|
+
summary: "UI and UX findings were inferred from the interaction audit.",
|
|
590
|
+
evidence: [],
|
|
591
|
+
blockers: []
|
|
592
|
+
});
|
|
593
|
+
const securityCoverage = coverageFrom(siteChecks.coverage.security, {
|
|
594
|
+
status: "blocked",
|
|
595
|
+
summary: "Security checks were unavailable for this run.",
|
|
596
|
+
evidence: [],
|
|
597
|
+
blockers: []
|
|
598
|
+
});
|
|
599
|
+
const technicalCoverage = coverageFrom(siteChecks.coverage.technicalHealth, {
|
|
600
|
+
status: "inferred",
|
|
601
|
+
summary: "Technical health relies on runtime browser signals.",
|
|
602
|
+
evidence: [],
|
|
603
|
+
blockers: []
|
|
604
|
+
});
|
|
605
|
+
const mobileCoverage = coverageFrom(siteChecks.coverage.mobileOptimization, {
|
|
606
|
+
status: "blocked",
|
|
607
|
+
summary: "Mobile checks were unavailable for this run.",
|
|
608
|
+
evidence: [],
|
|
609
|
+
blockers: []
|
|
610
|
+
});
|
|
611
|
+
const contentCoverage = coverageFrom(siteChecks.coverage.contentQuality, {
|
|
612
|
+
status: "blocked",
|
|
613
|
+
summary: "Content checks were unavailable for this run.",
|
|
614
|
+
evidence: [],
|
|
615
|
+
blockers: []
|
|
616
|
+
});
|
|
617
|
+
const croCoverage = coverageFrom(siteChecks.coverage.cro, {
|
|
618
|
+
status: "inferred",
|
|
619
|
+
summary: "CRO findings rely on visible task outcomes.",
|
|
620
|
+
evidence: [],
|
|
621
|
+
blockers: []
|
|
622
|
+
});
|
|
623
|
+
const seoCrawlSummary = siteChecks.seo.crawlSummary ?? {
|
|
624
|
+
totalPagesAudited: 0,
|
|
625
|
+
crawlDepthReached: 0,
|
|
626
|
+
pagesSkipped: 0,
|
|
627
|
+
skipReasons: []
|
|
628
|
+
};
|
|
629
|
+
const seoPageStats = siteChecks.seo.pageStats ?? {
|
|
630
|
+
pagesMissingTitle: 0,
|
|
631
|
+
pagesBadTitleLength: 0,
|
|
632
|
+
pagesMissingMetaDescription: 0,
|
|
633
|
+
pagesBadMetaDescriptionLength: 0,
|
|
634
|
+
pagesMissingCanonical: 0,
|
|
635
|
+
pagesNonSelfCanonical: 0,
|
|
636
|
+
noindexPages: 0,
|
|
637
|
+
nofollowPages: 0,
|
|
638
|
+
pagesMissingViewport: 0,
|
|
639
|
+
pagesMissingCharset: 0,
|
|
640
|
+
pagesWithStructuredData: 0,
|
|
641
|
+
pagesMissingOpenGraphBasics: 0,
|
|
642
|
+
pagesMissingTwitterCard: 0,
|
|
643
|
+
pagesWithUrlIssues: 0,
|
|
644
|
+
pagesMissingH1: 0,
|
|
645
|
+
pagesWithMultipleH1: 0,
|
|
646
|
+
pagesWithHeadingOrderIssues: 0,
|
|
647
|
+
pagesLowWordCount: 0,
|
|
648
|
+
pagesThinOrPlaceholder: 0,
|
|
649
|
+
pagesWithGenericAnchors: 0,
|
|
650
|
+
imagesMissingAlt: 0,
|
|
651
|
+
imagesWithNonDescriptiveFilenames: 0,
|
|
652
|
+
pagesWithRenderBlockingHeadScripts: 0,
|
|
653
|
+
pagesWithNonLazyImages: 0,
|
|
654
|
+
pagesWithResourceHints: 0,
|
|
655
|
+
pagesMissingLang: 0,
|
|
656
|
+
pagesWithUnlabeledInputs: 0,
|
|
657
|
+
pagesWithUnlabeledInteractive: 0,
|
|
658
|
+
pagesMissingSkipNav: 0
|
|
659
|
+
};
|
|
660
|
+
const seoAuditedPages = siteChecks.seo.auditedPages ?? [];
|
|
661
|
+
const executiveSummary = {
|
|
662
|
+
websiteUrl: args.website,
|
|
663
|
+
auditDate: formatAuditDate(args.startedAt, args.timeZone),
|
|
664
|
+
overallScore: formatScore(args.report.overall_score),
|
|
665
|
+
summary: ensureSentence(args.report.summary),
|
|
666
|
+
keyStrengths: uniqueItems(args.report.strengths.map((item) => ensureSentence(item)), 3),
|
|
667
|
+
criticalIssues: uniqueItems(args.report.weaknesses.map((item) => ensureSentence(item)), 3),
|
|
668
|
+
businessImpact: buildBusinessImpact(args.report)
|
|
669
|
+
};
|
|
670
|
+
const desktopDomVerification = probeVerification(siteChecks.performance.desktop, seoCoverage.status);
|
|
671
|
+
const mobileDomVerification = probeVerification(siteChecks.performance.mobile, mobileCoverage.status);
|
|
672
|
+
const desktopPerformanceVerification = performanceVerification(siteChecks.performance.desktop, performanceCoverage.status);
|
|
673
|
+
const mobilePerformanceVerification = performanceVerification(siteChecks.performance.mobile, mobileCoverage.status);
|
|
674
|
+
const seoCrawlVerification = siteChecks.seo.robotsTxt.statusCode !== null ||
|
|
675
|
+
siteChecks.seo.sitemap.statusCode !== null ||
|
|
676
|
+
siteChecks.seo.checkedLinkCount > 0 ||
|
|
677
|
+
seoCrawlSummary.totalPagesAudited > 0
|
|
678
|
+
? "verified"
|
|
679
|
+
: seoCoverage.status;
|
|
680
|
+
const securityHeaderVerification = siteChecks.security.initialStatusCode !== null ||
|
|
681
|
+
siteChecks.security.secureTransportVerified ||
|
|
682
|
+
siteChecks.security.securityHeaders.some((header) => /^Present with value|^Missing from the main document response\./i.test(header.note))
|
|
683
|
+
? "verified"
|
|
684
|
+
: securityCoverage.status;
|
|
685
|
+
const httpsVerification = siteChecks.security.initialStatusCode !== null || siteChecks.security.secureTransportVerified ? "verified" : securityCoverage.status;
|
|
686
|
+
const contentMetricVerification = siteChecks.performance.desktop?.loadOk
|
|
687
|
+
? "verified"
|
|
688
|
+
: siteChecks.performance.desktop &&
|
|
689
|
+
(siteChecks.performance.desktop.wordCount > 0 ||
|
|
690
|
+
siteChecks.performance.desktop.mediaCount > 0 ||
|
|
691
|
+
siteChecks.performance.desktop.readabilityScore !== null ||
|
|
692
|
+
siteChecks.performance.desktop.longParagraphCount > 0)
|
|
693
|
+
? "verified"
|
|
694
|
+
: contentCoverage.status;
|
|
695
|
+
const croMetricVerification = siteChecks.performance.desktop?.loadOk
|
|
696
|
+
? "verified"
|
|
697
|
+
: siteChecks.cro.ctaCount > 0 || siteChecks.cro.formCount > 0 || siteChecks.cro.trustSignalCount > 0
|
|
698
|
+
? "verified"
|
|
699
|
+
: croCoverage.status;
|
|
700
|
+
const performanceMetrics = [
|
|
701
|
+
metricFromMs("Desktop load time", siteChecks.performance.desktop?.performance.loadMs ?? null, desktopPerformanceVerification, { goodAtMost: 2500, warningAtMost: 4000 }, desktopPerformanceVerification === "blocked" ? performanceCoverage.blockers[0] ?? "Blocked" : "Load metric unavailable"),
|
|
702
|
+
metricFromMs("Mobile load time", siteChecks.performance.mobile?.performance.loadMs ?? null, mobilePerformanceVerification, { goodAtMost: 3000, warningAtMost: 4500 }, mobilePerformanceVerification === "blocked" ? mobileCoverage.blockers[0] ?? "Blocked" : "Load metric unavailable"),
|
|
703
|
+
metricFromMs("Largest Contentful Paint", siteChecks.performance.desktop?.performance.largestContentfulPaintMs ?? null, desktopPerformanceVerification, { goodAtMost: 2500, warningAtMost: 4000 }, desktopPerformanceVerification === "blocked" ? performanceCoverage.blockers[0] ?? "Blocked" : "LCP metric unavailable"),
|
|
704
|
+
metricFromRatio("Cumulative Layout Shift", siteChecks.performance.desktop?.performance.cumulativeLayoutShift ?? null, desktopPerformanceVerification, { goodAtMost: 0.1, warningAtMost: 0.25 }, 3, desktopPerformanceVerification === "blocked" ? performanceCoverage.blockers[0] ?? "Blocked" : "CLS metric unavailable"),
|
|
705
|
+
metricFromCount("Failed requests", siteChecks.performance.failedRequestCount, "verified", { goodAtMost: 0, warningAtMost: 2, singular: "failed request", plural: "failed requests" })
|
|
706
|
+
];
|
|
707
|
+
const seoGroups = [
|
|
708
|
+
{
|
|
709
|
+
title: "Crawl Coverage",
|
|
710
|
+
metrics: [
|
|
711
|
+
metricFromText("Pages audited", seoCrawlSummary.totalPagesAudited > 0
|
|
712
|
+
? `${seoCrawlSummary.totalPagesAudited} pages up to depth ${seoCrawlSummary.crawlDepthReached}`
|
|
713
|
+
: seoCrawlVerification === "blocked"
|
|
714
|
+
? seoCoverage.blockers[0] ?? "Blocked"
|
|
715
|
+
: "No crawl pages were captured", seoCrawlVerification === "blocked" ? "blocked" : seoCrawlSummary.totalPagesAudited >= 5 ? "good" : seoCrawlSummary.totalPagesAudited >= 1 ? "warning" : "poor", seoCrawlVerification),
|
|
716
|
+
metricFromText("Pages skipped", seoCrawlSummary.totalPagesAudited > 0 || seoCrawlSummary.pagesSkipped > 0
|
|
717
|
+
? `${seoCrawlSummary.pagesSkipped} skipped`
|
|
718
|
+
: seoCrawlVerification === "blocked"
|
|
719
|
+
? seoCoverage.blockers[0] ?? "Blocked"
|
|
720
|
+
: "No crawl skip data", seoCrawlVerification === "blocked"
|
|
721
|
+
? "blocked"
|
|
722
|
+
: seoCrawlSummary.pagesSkipped === 0
|
|
723
|
+
? "good"
|
|
724
|
+
: seoCrawlSummary.pagesSkipped <= Math.max(2, seoCrawlSummary.totalPagesAudited)
|
|
725
|
+
? "warning"
|
|
726
|
+
: "poor", seoCrawlVerification),
|
|
727
|
+
metricFromText("Top skip reasons", seoCrawlSummary.skipReasons.length > 0 ? seoCrawlSummary.skipReasons.join("; ") : "No major crawl skips recorded", seoCrawlVerification === "blocked" ? "blocked" : seoCrawlSummary.skipReasons.length === 0 ? "good" : "warning", seoCrawlVerification)
|
|
728
|
+
]
|
|
729
|
+
},
|
|
730
|
+
{
|
|
731
|
+
title: "Metadata Hygiene",
|
|
732
|
+
metrics: [
|
|
733
|
+
metricFromBoolean("Robots.txt", siteChecks.seo.robotsTxt.ok, siteChecks.seo.robotsTxt.statusCode !== null ? "verified" : seoCrawlVerification, "Present and reachable", siteChecks.seo.robotsTxt.note),
|
|
734
|
+
metricFromBoolean("Sitemap", siteChecks.seo.sitemap.ok, siteChecks.seo.sitemap.statusCode !== null ? "verified" : seoCrawlVerification, "Present and reachable", siteChecks.seo.sitemap.note),
|
|
735
|
+
metricFromAffectedPages("Title-tag issues", seoPageStats.pagesMissingTitle + seoPageStats.pagesBadTitleLength, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
736
|
+
goodAtMostPct: 10,
|
|
737
|
+
warningAtMostPct: 35
|
|
738
|
+
}, "Title-tag coverage unavailable"),
|
|
739
|
+
metricFromAffectedPages("Meta-description issues", seoPageStats.pagesMissingMetaDescription + seoPageStats.pagesBadMetaDescriptionLength, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
740
|
+
goodAtMostPct: 10,
|
|
741
|
+
warningAtMostPct: 35
|
|
742
|
+
}, "Meta-description coverage unavailable"),
|
|
743
|
+
metricFromAffectedPages("Canonical issues", seoPageStats.pagesMissingCanonical + seoPageStats.pagesNonSelfCanonical, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
744
|
+
goodAtMostPct: 5,
|
|
745
|
+
warningAtMostPct: 20
|
|
746
|
+
}, "Canonical coverage unavailable")
|
|
747
|
+
]
|
|
748
|
+
},
|
|
749
|
+
{
|
|
750
|
+
title: "Content & Indexability",
|
|
751
|
+
metrics: [
|
|
752
|
+
metricFromAffectedPages("H1 issues", seoPageStats.pagesMissingH1 + seoPageStats.pagesWithMultipleH1, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
753
|
+
goodAtMostPct: 5,
|
|
754
|
+
warningAtMostPct: 20
|
|
755
|
+
}, "Heading coverage unavailable"),
|
|
756
|
+
metricFromAffectedPages("Heading-order issues", seoPageStats.pagesWithHeadingOrderIssues, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
757
|
+
goodAtMostPct: 5,
|
|
758
|
+
warningAtMostPct: 20
|
|
759
|
+
}, "Heading-order coverage unavailable"),
|
|
760
|
+
metricFromAffectedPages("Low-word-count pages", seoPageStats.pagesLowWordCount, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
761
|
+
goodAtMostPct: 10,
|
|
762
|
+
warningAtMostPct: 30
|
|
763
|
+
}, "Word-count coverage unavailable"),
|
|
764
|
+
metricFromAffectedPages("Thin or placeholder pages", seoPageStats.pagesThinOrPlaceholder, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
765
|
+
goodAtMostPct: 5,
|
|
766
|
+
warningAtMostPct: 20
|
|
767
|
+
}, "Thin-content coverage unavailable"),
|
|
768
|
+
metricFromAffectedPages("Nofollow pages", seoPageStats.nofollowPages, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
769
|
+
goodAtMostPct: 5,
|
|
770
|
+
warningAtMostPct: 20
|
|
771
|
+
}, "Follow-directive coverage unavailable"),
|
|
772
|
+
metricFromAffectedPages("Noindex pages", seoPageStats.noindexPages, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
773
|
+
goodAtMostPct: 5,
|
|
774
|
+
warningAtMostPct: 20
|
|
775
|
+
}, "Indexability coverage unavailable")
|
|
776
|
+
]
|
|
777
|
+
},
|
|
778
|
+
{
|
|
779
|
+
title: "Search Enhancements",
|
|
780
|
+
metrics: [
|
|
781
|
+
metricFromAffectedPages("Missing Open Graph basics", seoPageStats.pagesMissingOpenGraphBasics, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
782
|
+
goodAtMostPct: 10,
|
|
783
|
+
warningAtMostPct: 35
|
|
784
|
+
}, "Open Graph coverage unavailable"),
|
|
785
|
+
metricFromAffectedPages("Missing Twitter cards", seoPageStats.pagesMissingTwitterCard, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
786
|
+
goodAtMostPct: 10,
|
|
787
|
+
warningAtMostPct: 35
|
|
788
|
+
}, "Twitter-card coverage unavailable"),
|
|
789
|
+
metricFromAffectedPages("Pages with URL issues", seoPageStats.pagesWithUrlIssues, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
790
|
+
goodAtMostPct: 5,
|
|
791
|
+
warningAtMostPct: 20
|
|
792
|
+
}, "URL-structure coverage unavailable"),
|
|
793
|
+
metricFromAffectedPages("Pages missing structured data", Math.max(0, seoCrawlSummary.totalPagesAudited - seoPageStats.pagesWithStructuredData), seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
794
|
+
goodAtMostPct: 15,
|
|
795
|
+
warningAtMostPct: 50
|
|
796
|
+
}, "Structured-data coverage unavailable")
|
|
797
|
+
]
|
|
798
|
+
},
|
|
799
|
+
{
|
|
800
|
+
title: "Links & HTML Signals",
|
|
801
|
+
metrics: [
|
|
802
|
+
metricFromCount("Sampled broken links", siteChecks.seo.brokenLinkCount, siteChecks.seo.checkedLinkCount > 0 ? "verified" : seoCrawlVerification, {
|
|
803
|
+
goodAtMost: 0,
|
|
804
|
+
warningAtMost: 1,
|
|
805
|
+
singular: "broken sampled link",
|
|
806
|
+
plural: "broken sampled links"
|
|
807
|
+
}),
|
|
808
|
+
metricFromAffectedPages("Generic-anchor pages", seoPageStats.pagesWithGenericAnchors, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
809
|
+
goodAtMostPct: 10,
|
|
810
|
+
warningAtMostPct: 35
|
|
811
|
+
}, "Anchor-text coverage unavailable"),
|
|
812
|
+
metricFromText("Images missing alt text", seoCrawlSummary.totalPagesAudited > 0 ? `${seoPageStats.imagesMissingAlt} image(s)` : seoCrawlVerification === "blocked" ? seoCoverage.blockers[0] ?? "Blocked" : "Image-alt coverage unavailable", seoCrawlVerification === "blocked"
|
|
813
|
+
? "blocked"
|
|
814
|
+
: seoPageStats.imagesMissingAlt === 0
|
|
815
|
+
? "good"
|
|
816
|
+
: seoPageStats.imagesMissingAlt <= Math.max(3, seoCrawlSummary.totalPagesAudited)
|
|
817
|
+
? "warning"
|
|
818
|
+
: "poor", seoCrawlVerification),
|
|
819
|
+
metricFromText("Non-descriptive image filenames", seoCrawlSummary.totalPagesAudited > 0 ? `${seoPageStats.imagesWithNonDescriptiveFilenames} image(s)` : seoCrawlVerification === "blocked" ? seoCoverage.blockers[0] ?? "Blocked" : "Image-filename coverage unavailable", seoCrawlVerification === "blocked"
|
|
820
|
+
? "blocked"
|
|
821
|
+
: seoPageStats.imagesWithNonDescriptiveFilenames === 0
|
|
822
|
+
? "good"
|
|
823
|
+
: seoPageStats.imagesWithNonDescriptiveFilenames <= Math.max(2, seoCrawlSummary.totalPagesAudited)
|
|
824
|
+
? "warning"
|
|
825
|
+
: "poor", seoCrawlVerification)
|
|
826
|
+
]
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
title: "Performance Signals",
|
|
830
|
+
metrics: [
|
|
831
|
+
metricFromAffectedPages("Viewport or charset missing", seoPageStats.pagesMissingViewport + seoPageStats.pagesMissingCharset, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
832
|
+
goodAtMostPct: 5,
|
|
833
|
+
warningAtMostPct: 15
|
|
834
|
+
}, "Viewport/charset coverage unavailable"),
|
|
835
|
+
metricFromAffectedPages("Render-blocking head scripts", seoPageStats.pagesWithRenderBlockingHeadScripts, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
836
|
+
goodAtMostPct: 10,
|
|
837
|
+
warningAtMostPct: 30
|
|
838
|
+
}, "Head-script coverage unavailable"),
|
|
839
|
+
metricFromAffectedPages("Pages with non-lazy images", seoPageStats.pagesWithNonLazyImages, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
840
|
+
goodAtMostPct: 10,
|
|
841
|
+
warningAtMostPct: 35
|
|
842
|
+
}, "Image-loading coverage unavailable"),
|
|
843
|
+
metricFromAffectedPages("Pages missing resource hints", Math.max(0, seoCrawlSummary.totalPagesAudited - seoPageStats.pagesWithResourceHints), seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
844
|
+
goodAtMostPct: 25,
|
|
845
|
+
warningAtMostPct: 60
|
|
846
|
+
}, "Resource-hint coverage unavailable")
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
{
|
|
850
|
+
title: "Accessibility Signals",
|
|
851
|
+
metrics: [
|
|
852
|
+
metricFromAffectedPages("Pages without lang", seoPageStats.pagesMissingLang, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
853
|
+
goodAtMostPct: 5,
|
|
854
|
+
warningAtMostPct: 15
|
|
855
|
+
}, "Lang coverage unavailable"),
|
|
856
|
+
metricFromAffectedPages("Pages with unlabeled form fields", seoPageStats.pagesWithUnlabeledInputs, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
857
|
+
goodAtMostPct: 5,
|
|
858
|
+
warningAtMostPct: 20
|
|
859
|
+
}, "Form-label coverage unavailable"),
|
|
860
|
+
metricFromAffectedPages("Pages with unlabeled controls", seoPageStats.pagesWithUnlabeledInteractive, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
861
|
+
goodAtMostPct: 5,
|
|
862
|
+
warningAtMostPct: 20
|
|
863
|
+
}, "Interactive-label coverage unavailable"),
|
|
864
|
+
metricFromAffectedPages("Pages missing skip nav", seoPageStats.pagesMissingSkipNav, seoCrawlSummary.totalPagesAudited, seoCrawlVerification, {
|
|
865
|
+
goodAtMostPct: 20,
|
|
866
|
+
warningAtMostPct: 50
|
|
867
|
+
}, "Skip-nav coverage unavailable")
|
|
868
|
+
]
|
|
869
|
+
},
|
|
870
|
+
...(seoAuditedPages.length > 0
|
|
871
|
+
? [
|
|
872
|
+
{
|
|
873
|
+
title: "Priority Pages",
|
|
874
|
+
metrics: seoAuditedPages.slice(0, 3).map((page, index) => metricFromText(`Page ${index + 1}`, `${page.url} (${page.issueCount} issue(s); ${page.notableIssues.slice(0, 2).join("; ") || "Needs manual follow-up"})`, page.issueCount <= 2 ? "good" : page.issueCount <= 5 ? "warning" : "poor", seoCrawlVerification))
|
|
875
|
+
}
|
|
876
|
+
]
|
|
877
|
+
: [])
|
|
878
|
+
];
|
|
879
|
+
const uiuxMetrics = [
|
|
880
|
+
metricFromText("Navigation clarity", formatScore(clampScore(average([args.report.scores.clarity, args.report.scores.navigation]))), labelFromScore(average([args.report.scores.clarity, args.report.scores.navigation])), uiuxCoverage.status),
|
|
881
|
+
metricFromText("Mobile responsiveness", siteChecks.mobileOptimization.responsiveVerdict === "responsive"
|
|
882
|
+
? "Responsive in dedicated mobile probe"
|
|
883
|
+
: siteChecks.mobileOptimization.responsiveVerdict === "mixed"
|
|
884
|
+
? "Partially responsive in dedicated mobile probe"
|
|
885
|
+
: siteChecks.mobileOptimization.responsiveVerdict === "poor"
|
|
886
|
+
? "Responsive issues found in dedicated mobile probe"
|
|
887
|
+
: mobileDomVerification === "blocked"
|
|
888
|
+
? mobileCoverage.blockers[0] ?? "Blocked"
|
|
889
|
+
: "Mobile probe did not load cleanly", siteChecks.mobileOptimization.responsiveVerdict === "responsive"
|
|
890
|
+
? "good"
|
|
891
|
+
: siteChecks.mobileOptimization.responsiveVerdict === "mixed"
|
|
892
|
+
? "warning"
|
|
893
|
+
: siteChecks.mobileOptimization.responsiveVerdict === "poor"
|
|
894
|
+
? "poor"
|
|
895
|
+
: mobileDomVerification === "blocked"
|
|
896
|
+
? "blocked"
|
|
897
|
+
: "poor", siteChecks.mobileOptimization.responsiveVerdict === "blocked" ? mobileDomVerification : "verified"),
|
|
898
|
+
metricFromText("Visual hierarchy", `Clarity score ${formatScore(args.report.scores.clarity)}`, labelFromScore(args.report.scores.clarity), uiuxCoverage.status),
|
|
899
|
+
buildAccessibilityMetric(args.accessibility)
|
|
900
|
+
];
|
|
901
|
+
const securityMetrics = [
|
|
902
|
+
metricFromBoolean("HTTPS", siteChecks.security.https, httpsVerification, "Enabled", "HTTP or blocked"),
|
|
903
|
+
metricFromBoolean("Secure transport", siteChecks.security.secureTransportVerified, siteChecks.security.initialStatusCode !== null ? "verified" : securityCoverage.status, "Verified by live page probe", securityCoverage.blockers[0] ?? "Could not verify secure transport"),
|
|
904
|
+
metricFromCount("Missing security headers", siteChecks.security.missingHeaders.length, securityHeaderVerification, {
|
|
905
|
+
goodAtMost: 0,
|
|
906
|
+
warningAtMost: 2,
|
|
907
|
+
singular: "missing header",
|
|
908
|
+
plural: "missing headers"
|
|
909
|
+
}),
|
|
910
|
+
metricFromText("Main document status", siteChecks.security.initialStatusCode ? `${siteChecks.security.initialStatusCode}` : securityCoverage.blockers[0] ?? "Blocked", siteChecks.security.initialStatusCode === null && securityCoverage.status === "blocked" ? "blocked" : siteChecks.security.initialStatusCode && siteChecks.security.initialStatusCode < 400 ? "good" : "poor", siteChecks.security.initialStatusCode !== null ? "verified" : securityCoverage.status)
|
|
911
|
+
];
|
|
912
|
+
const technicalMetrics = [
|
|
913
|
+
metricFromText("Framework/CMS", siteChecks.technicalHealth.framework ?? "Could not confidently fingerprint from sampled markup", siteChecks.technicalHealth.framework ? "good" : "warning", siteChecks.technicalHealth.framework ? "verified" : technicalCoverage.status),
|
|
914
|
+
metricFromCount("Console errors", siteChecks.technicalHealth.consoleErrorCount, technicalCoverage.status, {
|
|
915
|
+
goodAtMost: 0,
|
|
916
|
+
warningAtMost: 1,
|
|
917
|
+
singular: "console error",
|
|
918
|
+
plural: "console errors"
|
|
919
|
+
}),
|
|
920
|
+
metricFromCount("Page errors", siteChecks.technicalHealth.pageErrorCount, technicalCoverage.status, {
|
|
921
|
+
goodAtMost: 0,
|
|
922
|
+
warningAtMost: 1,
|
|
923
|
+
singular: "page error",
|
|
924
|
+
plural: "page errors"
|
|
925
|
+
}),
|
|
926
|
+
metricFromCount("API failures", siteChecks.technicalHealth.apiFailureCount, technicalCoverage.status, {
|
|
927
|
+
goodAtMost: 0,
|
|
928
|
+
warningAtMost: 1,
|
|
929
|
+
singular: "API failure",
|
|
930
|
+
plural: "API failures"
|
|
931
|
+
})
|
|
932
|
+
];
|
|
933
|
+
const mobileMetrics = [
|
|
934
|
+
metricFromBoolean("Dedicated mobile probe", siteChecks.mobileOptimization.mobile?.loadOk ?? false, mobileDomVerification, "Mobile page loaded successfully", mobileDomVerification === "blocked" ? mobileCoverage.blockers[0] ?? "Blocked" : "Mobile probe did not load cleanly"),
|
|
935
|
+
metricFromBoolean("Horizontal overflow", !(siteChecks.mobileOptimization.mobile?.horizontalOverflow ?? true), mobileDomVerification, "No horizontal overflow detected", mobileDomVerification === "blocked" ? mobileCoverage.blockers[0] ?? "Blocked" : "Horizontal overflow detected"),
|
|
936
|
+
metricFromCount("Tap target issues", siteChecks.mobileOptimization.mobile?.tapTargetIssueCount ?? 0, mobileDomVerification, {
|
|
937
|
+
goodAtMost: 0,
|
|
938
|
+
warningAtMost: 2,
|
|
939
|
+
singular: "tap target issue",
|
|
940
|
+
plural: "tap target issues"
|
|
941
|
+
}),
|
|
942
|
+
metricFromCount("Small-text issues", siteChecks.mobileOptimization.mobile?.smallTextIssueCount ?? 0, mobileDomVerification, {
|
|
943
|
+
goodAtMost: 0,
|
|
944
|
+
warningAtMost: 4,
|
|
945
|
+
singular: "small-text issue",
|
|
946
|
+
plural: "small-text issues"
|
|
947
|
+
})
|
|
948
|
+
];
|
|
949
|
+
const contentMetrics = [
|
|
950
|
+
metricFromText("Readability", siteChecks.contentQuality.readabilityScore !== null
|
|
951
|
+
? `${siteChecks.contentQuality.readabilityLabel} (${siteChecks.contentQuality.readabilityScore})`
|
|
952
|
+
: contentMetricVerification === "blocked"
|
|
953
|
+
? contentCoverage.blockers[0] ?? "Blocked"
|
|
954
|
+
: siteChecks.contentQuality.readabilityLabel || "Readability unavailable", contentMetricVerification === "blocked"
|
|
955
|
+
? "blocked"
|
|
956
|
+
: siteChecks.contentQuality.readabilityLabel === "Easy"
|
|
957
|
+
? "good"
|
|
958
|
+
: siteChecks.contentQuality.readabilityLabel === "Moderate"
|
|
959
|
+
? "warning"
|
|
960
|
+
: "poor", contentMetricVerification),
|
|
961
|
+
metricFromCount("Visible word count", siteChecks.contentQuality.wordCount, contentMetricVerification, {
|
|
962
|
+
goodAtMost: 999999,
|
|
963
|
+
warningAtMost: 120,
|
|
964
|
+
singular: "visible word",
|
|
965
|
+
plural: "visible words"
|
|
966
|
+
}),
|
|
967
|
+
metricFromCount("Long paragraphs", siteChecks.contentQuality.longParagraphCount, contentMetricVerification, {
|
|
968
|
+
goodAtMost: 0,
|
|
969
|
+
warningAtMost: 2,
|
|
970
|
+
singular: "long paragraph",
|
|
971
|
+
plural: "long paragraphs"
|
|
972
|
+
}),
|
|
973
|
+
metricFromCount("Media elements", siteChecks.contentQuality.mediaCount, contentMetricVerification, {
|
|
974
|
+
goodAtMost: 999999,
|
|
975
|
+
warningAtMost: 0,
|
|
976
|
+
singular: "media element",
|
|
977
|
+
plural: "media elements"
|
|
978
|
+
})
|
|
979
|
+
];
|
|
980
|
+
const croMetrics = [
|
|
981
|
+
metricFromCount("Visible CTA count", siteChecks.cro.ctaCount, croMetricVerification, {
|
|
982
|
+
goodAtMost: 999999,
|
|
983
|
+
warningAtMost: 1,
|
|
984
|
+
singular: "CTA",
|
|
985
|
+
plural: "CTAs"
|
|
986
|
+
}),
|
|
987
|
+
metricFromCount("Forms", siteChecks.cro.formCount, croMetricVerification, {
|
|
988
|
+
goodAtMost: 999999,
|
|
989
|
+
warningAtMost: 0,
|
|
990
|
+
singular: "form",
|
|
991
|
+
plural: "forms"
|
|
992
|
+
}),
|
|
993
|
+
metricFromCount("Trust signals", siteChecks.cro.trustSignalCount, croMetricVerification, {
|
|
994
|
+
goodAtMost: 999999,
|
|
995
|
+
warningAtMost: 0,
|
|
996
|
+
singular: "trust signal",
|
|
997
|
+
plural: "trust signals"
|
|
998
|
+
}),
|
|
999
|
+
metricFromText("Funnel clarity", formatScore(clampScore(average([args.report.scores.clarity, args.report.scores.conversion_readiness]))), labelFromScore(average([args.report.scores.clarity, args.report.scores.conversion_readiness])), croCoverage.status)
|
|
1000
|
+
];
|
|
1001
|
+
const performanceRecommendations = uniqueItems([
|
|
1002
|
+
...args.report.top_fixes,
|
|
1003
|
+
siteChecks.performance.imageFailureCount > 0 ? "Investigate the failing image requests that are weakening first-load stability." : "",
|
|
1004
|
+
siteChecks.performance.desktop?.performance.largestContentfulPaintMs && siteChecks.performance.desktop.performance.largestContentfulPaintMs > 2500
|
|
1005
|
+
? "Optimize the largest above-the-fold content so it paints faster."
|
|
1006
|
+
: ""
|
|
1007
|
+
].map((item) => ensureSentence(item)), 4);
|
|
1008
|
+
const seoRecommendations = uniqueItems([
|
|
1009
|
+
seoPageStats.pagesMissingTitle + seoPageStats.pagesBadTitleLength > 0 ? "Normalize page titles so each crawled page has a unique title in the ideal length range." : "",
|
|
1010
|
+
seoPageStats.pagesMissingMetaDescription + seoPageStats.pagesBadMetaDescriptionLength > 0 ? "Add or tighten meta descriptions across the pages missing strong search snippets." : "",
|
|
1011
|
+
seoPageStats.pagesMissingCanonical + seoPageStats.pagesNonSelfCanonical > 0 ? "Add self-referencing canonicals on the pages missing canonical consistency." : "",
|
|
1012
|
+
seoPageStats.pagesMissingH1 + seoPageStats.pagesWithMultipleH1 + seoPageStats.pagesWithHeadingOrderIssues > 0 ? "Normalize heading structure so pages expose one clear H1 and do not skip heading levels." : "",
|
|
1013
|
+
seoPageStats.pagesLowWordCount + seoPageStats.pagesThinOrPlaceholder > 0 ? "Expand thin pages with clearer, more useful copy and remove placeholder content." : "",
|
|
1014
|
+
seoPageStats.noindexPages + seoPageStats.nofollowPages > 0 ? "Review robots directives on pages marked noindex or nofollow and keep them only where they are intentional." : "",
|
|
1015
|
+
seoPageStats.pagesMissingOpenGraphBasics + seoPageStats.pagesMissingTwitterCard > 0 ? "Complete Open Graph and Twitter card metadata on pages missing social-preview tags." : "",
|
|
1016
|
+
siteChecks.seo.robotsTxt.ok ? "" : "Publish a reachable robots.txt file.",
|
|
1017
|
+
siteChecks.seo.sitemap.ok ? "" : "Publish a reachable sitemap.xml file.",
|
|
1018
|
+
siteChecks.seo.brokenLinks.length > 0 ? "Repair or redirect the failing sampled internal links." : "",
|
|
1019
|
+
seoPageStats.imagesMissingAlt > 0 ? "Add descriptive alt text to images that currently have empty or missing alt attributes." : "",
|
|
1020
|
+
seoPageStats.pagesWithUnlabeledInputs + seoPageStats.pagesWithUnlabeledInteractive > 0 ? "Add clear labels or accessible names to form fields and interactive controls." : "",
|
|
1021
|
+
seoPageStats.pagesMissingLang + seoPageStats.pagesMissingSkipNav > 0 ? "Add page language declarations and skip-navigation links to improve crawl clarity and accessibility." : "",
|
|
1022
|
+
seoPageStats.pagesWithNonLazyImages + Math.max(0, seoCrawlSummary.totalPagesAudited - seoPageStats.pagesWithResourceHints) > 0
|
|
1023
|
+
? "Defer non-critical imagery and add preload, preconnect, or similar resource hints where they materially help the critical path."
|
|
1024
|
+
: "",
|
|
1025
|
+
seoPageStats.pagesWithRenderBlockingHeadScripts > 0 ? "Defer or async non-critical head scripts that are blocking crawl and render efficiency." : "",
|
|
1026
|
+
seoAuditedPages[0] ? `Start with ${seoAuditedPages[0].url}, which surfaced ${seoAuditedPages[0].issueCount} SEO issue(s) in the crawl.` : ""
|
|
1027
|
+
].map((item) => ensureSentence(item)), 8);
|
|
1028
|
+
const uiuxRecommendations = uniqueItems([...args.report.top_fixes, ...mobileCoverage.evidence].map((item) => ensureSentence(item)), 4);
|
|
1029
|
+
const securityRecommendations = uniqueItems([
|
|
1030
|
+
siteChecks.security.missingHeaders.length > 0 ? `Add the missing security headers: ${siteChecks.security.missingHeaders.join(", ")}.` : "",
|
|
1031
|
+
siteChecks.security.https ? "" : "Serve the main experience over HTTPS.",
|
|
1032
|
+
signals.securityBarriers > 0 ? "Provide a QA-safe lane when verification walls block normal task coverage." : ""
|
|
1033
|
+
].map((item) => ensureSentence(item)), 4);
|
|
1034
|
+
const technicalRecommendations = uniqueItems([
|
|
1035
|
+
siteChecks.technicalHealth.consoleErrorCount > 0 ? "Fix the console errors surfaced during the run." : "",
|
|
1036
|
+
siteChecks.technicalHealth.pageErrorCount > 0 ? "Fix the uncaught runtime page errors." : "",
|
|
1037
|
+
siteChecks.technicalHealth.apiFailureCount > 0 ? "Stabilize the failing API requests and backend responses." : ""
|
|
1038
|
+
].map((item) => ensureSentence(item)), 4);
|
|
1039
|
+
const mobileRecommendations = uniqueItems([
|
|
1040
|
+
siteChecks.mobileOptimization.mobile?.horizontalOverflow ? "Fix horizontal overflow in the mobile layout." : "",
|
|
1041
|
+
(siteChecks.mobileOptimization.mobile?.tapTargetIssueCount ?? 0) > 0 ? "Increase the size of undersized mobile tap targets." : "",
|
|
1042
|
+
(siteChecks.mobileOptimization.mobile?.smallTextIssueCount ?? 0) > 0 ? "Increase mobile text size where content falls below the legibility threshold." : ""
|
|
1043
|
+
].map((item) => ensureSentence(item)), 4);
|
|
1044
|
+
const contentRecommendations = uniqueItems([
|
|
1045
|
+
siteChecks.contentQuality.longParagraphCount > 0 ? "Break long paragraphs into shorter, more scannable blocks." : "",
|
|
1046
|
+
siteChecks.contentQuality.mediaCount === 0 ? "Add supporting media where visuals would help explain the offer faster." : "",
|
|
1047
|
+
args.report.scores.clarity <= 6 ? "Tighten headlines and supporting copy around the primary action." : ""
|
|
1048
|
+
].map((item) => ensureSentence(item)), 4);
|
|
1049
|
+
const croRecommendations = uniqueItems([
|
|
1050
|
+
siteChecks.cro.ctaCount === 0 ? "Expose a clearer primary CTA above the fold." : "",
|
|
1051
|
+
siteChecks.cro.formCount === 0 ? "If lead capture matters, expose a clear form or conversion path on the sampled page." : "",
|
|
1052
|
+
siteChecks.cro.trustSignalCount === 0 ? "Add stronger trust signals near the main CTA." : "",
|
|
1053
|
+
...args.report.top_fixes
|
|
1054
|
+
].map((item) => ensureSentence(item)), 4);
|
|
1055
|
+
const scoreBreakdown = [
|
|
1056
|
+
{ category: "Performance", score: formatScore(performanceScore) },
|
|
1057
|
+
{ category: "SEO", score: formatScore(seoScore) },
|
|
1058
|
+
{ category: "UI/UX", score: formatScore(uiuxScore) },
|
|
1059
|
+
{ category: "Security", score: formatScore(securityScore) },
|
|
1060
|
+
{ category: "Mobile", score: formatScore(mobileScore) },
|
|
1061
|
+
{ category: "Overall", score: formatScore(args.report.overall_score) }
|
|
1062
|
+
];
|
|
1063
|
+
const limitations = uniqueItems([
|
|
1064
|
+
...performanceCoverage.blockers,
|
|
1065
|
+
...seoCoverage.blockers,
|
|
1066
|
+
...securityCoverage.blockers,
|
|
1067
|
+
...mobileCoverage.blockers,
|
|
1068
|
+
args.accessibility?.error ? ensureSentence(args.accessibility.error) : ""
|
|
1069
|
+
], 6);
|
|
1070
|
+
return {
|
|
1071
|
+
executiveSummary,
|
|
1072
|
+
performance: {
|
|
1073
|
+
coverage: performanceCoverage,
|
|
1074
|
+
tools: ["Dedicated desktop and mobile page probes", "Saved runtime network and interaction signals"],
|
|
1075
|
+
metrics: performanceMetrics,
|
|
1076
|
+
insights: uniqueItems([...siteChecks.performance.evidence, ...performanceCoverage.evidence], 5),
|
|
1077
|
+
recommendations: performanceRecommendations
|
|
1078
|
+
},
|
|
1079
|
+
seo: {
|
|
1080
|
+
coverage: seoCoverage,
|
|
1081
|
+
tools: ["Live DOM metadata probe", "Same-origin HTML crawl", "robots.txt fetch", "sitemap.xml fetch", "Sampled internal-link checks"],
|
|
1082
|
+
groups: seoGroups,
|
|
1083
|
+
recommendations: seoRecommendations
|
|
1084
|
+
},
|
|
1085
|
+
uiux: {
|
|
1086
|
+
coverage: uiuxCoverage,
|
|
1087
|
+
metrics: uiuxMetrics,
|
|
1088
|
+
issues: uniqueItems([...args.report.weaknesses, ...uiuxCoverage.evidence].map((item) => ensureSentence(item)), 5),
|
|
1089
|
+
recommendations: uiuxRecommendations
|
|
1090
|
+
},
|
|
1091
|
+
security: {
|
|
1092
|
+
coverage: securityCoverage,
|
|
1093
|
+
tools: ["Live document-response header probe", "HTTPS transport verification"],
|
|
1094
|
+
metrics: securityMetrics,
|
|
1095
|
+
recommendations: securityRecommendations
|
|
1096
|
+
},
|
|
1097
|
+
technicalHealth: {
|
|
1098
|
+
coverage: technicalCoverage,
|
|
1099
|
+
metrics: technicalMetrics,
|
|
1100
|
+
recommendations: technicalRecommendations
|
|
1101
|
+
},
|
|
1102
|
+
mobileOptimization: {
|
|
1103
|
+
coverage: mobileCoverage,
|
|
1104
|
+
metrics: mobileMetrics,
|
|
1105
|
+
recommendations: mobileRecommendations
|
|
1106
|
+
},
|
|
1107
|
+
contentQuality: {
|
|
1108
|
+
coverage: contentCoverage,
|
|
1109
|
+
metrics: contentMetrics,
|
|
1110
|
+
recommendations: contentRecommendations
|
|
1111
|
+
},
|
|
1112
|
+
cro: {
|
|
1113
|
+
coverage: croCoverage,
|
|
1114
|
+
metrics: croMetrics,
|
|
1115
|
+
recommendations: croRecommendations
|
|
1116
|
+
},
|
|
1117
|
+
actionPlan: buildActionPlan({
|
|
1118
|
+
report: args.report,
|
|
1119
|
+
accessibility: args.accessibility,
|
|
1120
|
+
siteChecks
|
|
1121
|
+
}),
|
|
1122
|
+
scoreBreakdown,
|
|
1123
|
+
agentNotes: {
|
|
1124
|
+
confidence: buildConfidence({
|
|
1125
|
+
report: args.report,
|
|
1126
|
+
taskResults: args.taskResults,
|
|
1127
|
+
accessibility: args.accessibility,
|
|
1128
|
+
siteChecks,
|
|
1129
|
+
signals
|
|
1130
|
+
}),
|
|
1131
|
+
dataSources: uniqueItems([
|
|
1132
|
+
"Task outcomes and step-by-step interaction histories",
|
|
1133
|
+
(args.rawEvents?.length ?? 0) > 0 ? "Saved browser raw events and failed request logs" : "",
|
|
1134
|
+
args.accessibility ? "Automated accessibility scan output" : "",
|
|
1135
|
+
args.siteChecks ? "Supplemental desktop and mobile site checks artifact" : "",
|
|
1136
|
+
args.mobile ? "Primary run used a mobile-sized browser" : "Primary run used a desktop-sized browser"
|
|
1137
|
+
], 5),
|
|
1138
|
+
limitations
|
|
1139
|
+
}
|
|
1140
|
+
};
|
|
1141
|
+
}
|