@aexol/spectral 0.9.126 → 0.9.128
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/dist/agent/index.d.ts.map +1 -1
- package/dist/agent/index.js +5 -0
- package/dist/extensions/seo/analyzers/content.d.ts +29 -6
- package/dist/extensions/seo/analyzers/content.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/content.js +243 -8
- package/dist/extensions/seo/analyzers/crawler.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/crawler.js +133 -7
- package/dist/extensions/seo/analyzers/drift.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/drift.js +86 -29
- package/dist/extensions/seo/analyzers/images.d.ts +10 -4
- package/dist/extensions/seo/analyzers/images.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/images.js +153 -5
- package/dist/extensions/seo/analyzers/performance.d.ts +3 -3
- package/dist/extensions/seo/analyzers/performance.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/performance.js +77 -6
- package/dist/extensions/seo/analyzers/readability.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/readability.js +17 -9
- package/dist/extensions/seo/analyzers/schema.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/schema.js +99 -10
- package/dist/extensions/seo/analyzers/sitemap.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/sitemap.js +57 -16
- package/dist/extensions/seo/analyzers/technical.d.ts +0 -7
- package/dist/extensions/seo/analyzers/technical.d.ts.map +1 -1
- package/dist/extensions/seo/analyzers/technical.js +408 -37
- package/dist/extensions/seo/config.d.ts +2 -0
- package/dist/extensions/seo/config.d.ts.map +1 -1
- package/dist/extensions/seo/config.js +2 -0
- package/dist/extensions/seo/tools/batch.js +1 -1
- package/dist/extensions/seo/tools/content.d.ts.map +1 -1
- package/dist/extensions/seo/tools/content.js +5 -3
- package/dist/extensions/seo/tools/full-audit.d.ts +1 -1
- package/dist/extensions/seo/tools/full-audit.d.ts.map +1 -1
- package/dist/extensions/seo/tools/full-audit.js +391 -66
- package/dist/extensions/seo/tools/helpers.d.ts +15 -4
- package/dist/extensions/seo/tools/helpers.d.ts.map +1 -1
- package/dist/extensions/seo/tools/helpers.js +57 -6
- package/dist/extensions/seo/tools/performance.d.ts.map +1 -1
- package/dist/extensions/seo/tools/performance.js +17 -11
- package/dist/extensions/seo/tools/report.d.ts.map +1 -1
- package/dist/extensions/seo/tools/report.js +513 -47
- package/dist/extensions/seo/types.d.ts +295 -3
- package/dist/extensions/seo/types.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.d.ts +4 -0
- package/dist/extensions/seo/utils/fetcher.d.ts.map +1 -1
- package/dist/extensions/seo/utils/fetcher.js +77 -11
- package/dist/extensions/seo/utils/parser.d.ts +3 -1
- package/dist/extensions/seo/utils/parser.d.ts.map +1 -1
- package/dist/extensions/seo/utils/parser.js +105 -25
- package/dist/extensions/seo/utils/renderer.d.ts +6 -1
- package/dist/extensions/seo/utils/renderer.d.ts.map +1 -1
- package/dist/extensions/seo/utils/renderer.js +207 -1
- package/dist/extensions/seo/utils/url-safety.d.ts.map +1 -1
- package/dist/extensions/seo/utils/url-safety.js +19 -3
- package/dist/extensions/web/tools/fetch.js +2 -2
- package/dist/memory/tools/compact-context.d.ts.map +1 -1
- package/dist/memory/tools/compact-context.js +10 -3
- package/dist/sdk/agent-core/agent-loop.d.ts.map +1 -1
- package/dist/sdk/agent-core/agent-loop.js +87 -4
- package/dist/sdk/agent-core/types.d.ts +1 -0
- package/dist/sdk/agent-core/types.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.d.ts +0 -7
- package/dist/sdk/ai/utils/validation.d.ts.map +1 -1
- package/dist/sdk/ai/utils/validation.js +21 -1
- package/dist/sdk/coding-agent/core/extensions/runner.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/extensions/runner.js +15 -1
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts +7 -2
- package/dist/sdk/coding-agent/core/tools/apply-patch.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/apply-patch.js +219 -41
- package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts +30 -0
- package/dist/sdk/coding-agent/core/tools/atomic-write.d.ts.map +1 -0
- package/dist/sdk/coding-agent/core/tools/atomic-write.js +42 -0
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts +10 -4
- package/dist/sdk/coding-agent/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit-diff.js +44 -24
- package/dist/sdk/coding-agent/core/tools/edit.d.ts +6 -0
- package/dist/sdk/coding-agent/core/tools/edit.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/edit.js +31 -5
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts +9 -3
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/file-mutation-queue.js +42 -3
- package/dist/sdk/coding-agent/core/tools/read.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/read.js +79 -47
- package/dist/sdk/coding-agent/core/tools/truncate.d.ts +4 -2
- package/dist/sdk/coding-agent/core/tools/truncate.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/truncate.js +43 -23
- package/dist/sdk/coding-agent/core/tools/write.d.ts +5 -8
- package/dist/sdk/coding-agent/core/tools/write.d.ts.map +1 -1
- package/dist/sdk/coding-agent/core/tools/write.js +29 -11
- package/package.json +1 -1
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { auditTechnical } from "../analyzers/technical.js";
|
|
2
2
|
import { analyzeContentQuality } from "../analyzers/content.js";
|
|
3
3
|
import { auditSchema } from "../analyzers/schema.js";
|
|
4
|
-
import { auditImages } from "../analyzers/images.js";
|
|
4
|
+
import { auditImages, checkImageUrls } from "../analyzers/images.js";
|
|
5
5
|
import { auditPreload } from "../analyzers/preload.js";
|
|
6
6
|
import { auditAgentUx } from "../analyzers/agent-ux.js";
|
|
7
|
+
import { calculatePerformanceFromFetch, analyzePageWeight } from "../analyzers/performance.js";
|
|
7
8
|
import { fetchPage } from "../utils/fetcher.js";
|
|
8
9
|
import { renderPage, isRendererAvailable } from "../utils/renderer.js";
|
|
9
10
|
import { parsePage, extractText } from "../utils/parser.js";
|
|
10
|
-
import { errorResult, textResult } from "./helpers.js";
|
|
11
|
-
const ALL_CATEGORIES = ["technical", "content", "schema", "images", "preload", "agentUx"];
|
|
11
|
+
import { errorResult, textResult, validateToolUrl } from "./helpers.js";
|
|
12
|
+
const ALL_CATEGORIES = ["technical", "content", "schema", "images", "preload", "agentUx", "performance"];
|
|
12
13
|
export function registerFullAuditTools(ext, config) {
|
|
13
14
|
const fullAuditTool = {
|
|
14
15
|
name: "seo_audit",
|
|
@@ -17,8 +18,9 @@ export function registerFullAuditTools(ext, config) {
|
|
|
17
18
|
"content quality (filler, AI patterns, density), schema audit (detection, validation), image audit (alt, dimensions, formats), " +
|
|
18
19
|
"preload audit (speculation rules, bfcache), and agent UX audit (accessibility, semantic HTML). " +
|
|
19
20
|
"Returns an overall score and all issues prioritized by severity. " +
|
|
20
|
-
"Use 'include' to run only specific sub-audits."
|
|
21
|
-
|
|
21
|
+
"Use 'include' to run only specific sub-audits. " +
|
|
22
|
+
"When 'performance' is included and a browser is available, real Web Vitals are measured via Playwright.",
|
|
23
|
+
promptSnippet: "`seo_audit { url, include? }` — full SEO audit (technical + content + schema + images + preload + agentUx + performance)",
|
|
22
24
|
promptGuidelines: [
|
|
23
25
|
"When the user asks about SEO, search optimization, page audits, schema markup, sitemaps, Core Web Vitals, or technical SEO for any website or URL, use the seo_* tools. Start with seo_audit for comprehensive analysis — it runs all sub-audits in one call. For specific concerns use the targeted tools (seo_technical_audit, seo_content_quality, seo_schema_audit, seo_image_audit, seo_preload_audit, seo_agent_ux_audit). These tools are offline-first and require no API keys.",
|
|
24
26
|
],
|
|
@@ -30,7 +32,7 @@ export function registerFullAuditTools(ext, config) {
|
|
|
30
32
|
type: "array",
|
|
31
33
|
items: {
|
|
32
34
|
type: "string",
|
|
33
|
-
enum: ["technical", "content", "schema", "images", "preload", "agentUx"],
|
|
35
|
+
enum: ["technical", "content", "schema", "images", "preload", "agentUx", "performance"],
|
|
34
36
|
},
|
|
35
37
|
description: "Sub-audits to include. If omitted, runs all sub-audits.",
|
|
36
38
|
},
|
|
@@ -38,8 +40,9 @@ export function registerFullAuditTools(ext, config) {
|
|
|
38
40
|
required: ["url"],
|
|
39
41
|
},
|
|
40
42
|
async execute(_toolCallId, params) {
|
|
41
|
-
const { url, include } = params;
|
|
43
|
+
const { url: rawUrl, include } = params;
|
|
42
44
|
try {
|
|
45
|
+
const url = validateToolUrl(rawUrl);
|
|
43
46
|
const categories = include ?? ALL_CATEGORIES;
|
|
44
47
|
const result = await runFullAudit(url, config, categories);
|
|
45
48
|
const issueLines = result.allIssues
|
|
@@ -49,14 +52,18 @@ export function registerFullAuditTools(ext, config) {
|
|
|
49
52
|
.join("\n");
|
|
50
53
|
const summary = `✓ Full SEO Audit: ${result.url}\n` +
|
|
51
54
|
`Audited At: ${result.auditedAt}\n` +
|
|
52
|
-
`Overall Score: ${result.overallScore}/100\n
|
|
55
|
+
`Overall Score: ${result.overallScore}/100\n` +
|
|
56
|
+
`Coverage: ${result.coverage}% | Confidence: ${result.confidence}\n` +
|
|
57
|
+
`Skipped: ${result.skippedCategories.join(", ") || "none"}\n` +
|
|
53
58
|
`Sub-audits run:\n` +
|
|
54
59
|
` Technical: ${result.technical ? `${result.technical.score}/100` : "skipped"}\n` +
|
|
55
60
|
` Content: ${result.content ? `${result.content.overallQuality}/100` : "skipped"}\n` +
|
|
56
61
|
` Schema: ${result.schema ? `${result.schema.detectedSchemas.length} blocks` : "skipped"}\n` +
|
|
57
62
|
` Images: ${result.images ? `${result.images.missingAlt} missing alt` : "skipped"}\n` +
|
|
58
63
|
` Preload: ${result.preload ? `${result.preload.score}/100` : "skipped"}\n` +
|
|
59
|
-
` Agent UX: ${result.agentUx ? `${result.agentUx.agentUxScore}/100` : "skipped"}\n
|
|
64
|
+
` Agent UX: ${result.agentUx ? `${result.agentUx.agentUxScore}/100` : "skipped"}\n` +
|
|
65
|
+
` Performance: ${result.performance ? `${result.performance.score}/100 (${result.performance.source ?? "heuristic"})` : "skipped"}\n` +
|
|
66
|
+
`Renderer: ${result.rendererAvailable ? "available" : "unavailable"}${result.renderComparison ? ` | Render diff: ${result.renderComparison.textDelta > 0 ? "+" : ""}${result.renderComparison.textDelta} words${result.renderComparison.isSpa ? " (SPA detected)" : ""}` : ""}\n\n` +
|
|
60
67
|
`Top Issues (${result.allIssues.length} total):\n${issueLines || " (none)"}`;
|
|
61
68
|
return textResult(summary, { result });
|
|
62
69
|
}
|
|
@@ -70,89 +77,316 @@ export function registerFullAuditTools(ext, config) {
|
|
|
70
77
|
async function runFullAudit(url, config, categories) {
|
|
71
78
|
// Fetch the page once and reuse for all sub-audits.
|
|
72
79
|
const fetchResult = await fetchPage(url, config);
|
|
73
|
-
const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
|
|
74
80
|
const allIssues = [];
|
|
75
|
-
|
|
76
|
-
//
|
|
77
|
-
|
|
81
|
+
const skippedCategories = ALL_CATEGORIES.filter((category) => !categories.includes(category));
|
|
82
|
+
// Handle fetch failures as structured audit issues instead of generic errors.
|
|
83
|
+
if (fetchResult.error || fetchResult.statusCode === 0 || fetchResult.statusCode >= 400) {
|
|
84
|
+
const severity = fetchResult.statusCode >= 500 || fetchResult.statusCode === 0 ? "critical" : "high";
|
|
85
|
+
const title = fetchResult.statusCode === 0
|
|
86
|
+
? `Fetch failed: ${fetchResult.error ?? "unknown error"}`
|
|
87
|
+
: `HTTP ${fetchResult.statusCode} ${fetchResult.statusText}`;
|
|
88
|
+
allIssues.push({
|
|
89
|
+
category: "Fetching",
|
|
90
|
+
severity,
|
|
91
|
+
title,
|
|
92
|
+
description: fetchResult.error
|
|
93
|
+
? `Unable to fetch ${url}: ${fetchResult.error}`
|
|
94
|
+
: `Server returned ${fetchResult.statusCode} ${fetchResult.statusText}.`,
|
|
95
|
+
recommendation: fetchResult.statusCode >= 500
|
|
96
|
+
? "Investigate server-side errors; the page may be temporarily unavailable."
|
|
97
|
+
: fetchResult.statusCode === 404
|
|
98
|
+
? "The page was not found. Check the URL or restore the resource."
|
|
99
|
+
: fetchResult.statusCode === 0
|
|
100
|
+
? "Verify DNS, network connectivity, and that the server is reachable."
|
|
101
|
+
: "Review server configuration and access controls.",
|
|
102
|
+
});
|
|
103
|
+
const summary = `SEO audit for ${url} — fetch failed (${fetchResult.statusCode || "error"}), ${allIssues.length} issues found.`;
|
|
104
|
+
return {
|
|
105
|
+
url,
|
|
106
|
+
auditedAt: new Date().toISOString(),
|
|
107
|
+
overallScore: 0,
|
|
108
|
+
coverage: 0,
|
|
109
|
+
confidence: "low",
|
|
110
|
+
skippedCategories: ALL_CATEGORIES,
|
|
111
|
+
rendererAvailable: false,
|
|
112
|
+
version: 2,
|
|
113
|
+
warnings: ["Fetch failed, sub-audits were not run."],
|
|
114
|
+
allIssues,
|
|
115
|
+
summary,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
const pageData = parsePage(fetchResult.content, fetchResult.finalUrl);
|
|
119
|
+
// Concurrent sub-audit execution. Each sub-audit runs independently; a
|
|
120
|
+
// failure in one does not crash the others (Promise.allSettled).
|
|
121
|
+
const scores = [];
|
|
122
|
+
const controller = new AbortController();
|
|
123
|
+
const timeout = setTimeout(() => controller.abort(), config.timeout * 4);
|
|
124
|
+
const subAuditFns = [];
|
|
78
125
|
if (categories.includes("technical")) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
126
|
+
subAuditFns.push(async () => {
|
|
127
|
+
const technical = await auditTechnical(url, config, pageData, fetchResult);
|
|
128
|
+
return {
|
|
129
|
+
key: "technical",
|
|
130
|
+
score: technical.score,
|
|
131
|
+
issues: technical.issues,
|
|
132
|
+
result: technical,
|
|
133
|
+
};
|
|
134
|
+
});
|
|
82
135
|
}
|
|
83
|
-
// Content quality.
|
|
84
|
-
let content;
|
|
85
136
|
if (categories.includes("content")) {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
137
|
+
subAuditFns.push(async () => {
|
|
138
|
+
const text = extractText(fetchResult.content);
|
|
139
|
+
const content = analyzeContentQuality(text, fetchResult.finalUrl, pageData);
|
|
140
|
+
return {
|
|
141
|
+
key: "content",
|
|
142
|
+
score: content.overallQuality,
|
|
143
|
+
issues: [],
|
|
144
|
+
result: content,
|
|
145
|
+
};
|
|
146
|
+
});
|
|
89
147
|
}
|
|
90
|
-
// Schema audit.
|
|
91
|
-
let schema;
|
|
92
148
|
if (categories.includes("schema")) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
allIssues.push({
|
|
149
|
+
subAuditFns.push(async () => {
|
|
150
|
+
const schema = auditSchema(fetchResult.content, fetchResult.finalUrl);
|
|
151
|
+
const issues = schema.recommendations.map((rec) => ({
|
|
97
152
|
category: "Schema",
|
|
98
153
|
severity: "medium",
|
|
99
154
|
title: rec,
|
|
100
155
|
description: rec,
|
|
101
156
|
recommendation: rec,
|
|
102
|
-
});
|
|
103
|
-
|
|
157
|
+
}));
|
|
158
|
+
return {
|
|
159
|
+
key: "schema",
|
|
160
|
+
score: schema.score,
|
|
161
|
+
issues,
|
|
162
|
+
result: schema,
|
|
163
|
+
};
|
|
164
|
+
});
|
|
104
165
|
}
|
|
105
|
-
// Image audit.
|
|
106
|
-
let images;
|
|
107
166
|
if (categories.includes("images")) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
167
|
+
subAuditFns.push(async () => {
|
|
168
|
+
const images = auditImages(fetchResult.content, fetchResult.finalUrl, pageData);
|
|
169
|
+
const issues = [];
|
|
170
|
+
if (images.missingAlt > 0) {
|
|
171
|
+
issues.push({
|
|
172
|
+
category: "Images",
|
|
173
|
+
severity: "medium",
|
|
174
|
+
title: `${images.missingAlt} images missing alt text`,
|
|
175
|
+
description: `${images.missingAlt} of ${images.totalImages} images have no alt attribute.`,
|
|
176
|
+
recommendation: "Add descriptive alt text to all informative images.",
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
if (images.missingDimensions > 0) {
|
|
180
|
+
issues.push({
|
|
181
|
+
category: "Images",
|
|
182
|
+
severity: "low",
|
|
183
|
+
title: `${images.missingDimensions} images missing dimensions`,
|
|
184
|
+
description: `${images.missingDimensions} images lack width/height attributes, causing layout shift.`,
|
|
185
|
+
recommendation: "Add width and height attributes to prevent CLS.",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (images.fetchpriorityIssues?.length) {
|
|
189
|
+
for (const fi of images.fetchpriorityIssues) {
|
|
190
|
+
issues.push({
|
|
191
|
+
category: "Images",
|
|
192
|
+
severity: "medium",
|
|
193
|
+
title: fi,
|
|
194
|
+
description: fi,
|
|
195
|
+
recommendation: "Set fetchpriority=\"high\" on the LCP image and avoid it on small images.",
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
if (images.ratioIssues?.length) {
|
|
200
|
+
issues.push({
|
|
201
|
+
category: "Images",
|
|
202
|
+
severity: "low",
|
|
203
|
+
title: `${images.ratioIssues.length} image(s) with extreme aspect ratios`,
|
|
204
|
+
description: `Extreme aspect ratios detected: ${images.ratioIssues.join("; ")}`,
|
|
205
|
+
recommendation: "Review images with extreme aspect ratios; they may appear distorted.",
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
if (images.responsiveCoverage !== undefined && images.responsiveCoverage < 50) {
|
|
209
|
+
issues.push({
|
|
210
|
+
category: "Images",
|
|
211
|
+
severity: "medium",
|
|
212
|
+
title: `Low responsive image coverage (${images.responsiveCoverage}%)`,
|
|
213
|
+
description: `Only ${images.responsiveImages ?? 0} of ${images.totalImages} images use srcset, sizes, or picture elements.`,
|
|
214
|
+
recommendation: "Add srcset and sizes attributes for responsive delivery.",
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
if (images.svgMissingTitle && images.svgMissingTitle > 0) {
|
|
218
|
+
issues.push({
|
|
219
|
+
category: "Images",
|
|
220
|
+
severity: "medium",
|
|
221
|
+
title: `${images.svgMissingTitle} SVG(s) missing accessible title`,
|
|
222
|
+
description: `${images.svgMissingTitle} inline SVGs lack a <title>, role="img", or aria-label.`,
|
|
223
|
+
recommendation: "Add <title>, <desc>, or role=\"img\" with aria-label to SVGs.",
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
// Check for broken image URLs
|
|
227
|
+
if (images.images.length > 0) {
|
|
228
|
+
const { brokenUrls } = await checkImageUrls(images.images, config);
|
|
229
|
+
for (const url of brokenUrls) {
|
|
230
|
+
issues.push({
|
|
231
|
+
category: "Images",
|
|
232
|
+
severity: "high",
|
|
233
|
+
title: `Broken image: ${url}`,
|
|
234
|
+
description: `Image returned a non-2xx status code: ${url}`,
|
|
235
|
+
recommendation: "Replace or remove the broken image.",
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
key: "images",
|
|
241
|
+
score: images.score,
|
|
242
|
+
issues,
|
|
243
|
+
result: images,
|
|
244
|
+
};
|
|
245
|
+
});
|
|
127
246
|
}
|
|
128
|
-
// Preload audit.
|
|
129
|
-
let preload;
|
|
130
247
|
if (categories.includes("preload")) {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
allIssues.push({
|
|
248
|
+
subAuditFns.push(async () => {
|
|
249
|
+
const preload = auditPreload(fetchResult);
|
|
250
|
+
const issues = preload.recommendations.map((rec) => ({
|
|
135
251
|
category: "Preload",
|
|
136
252
|
severity: "medium",
|
|
137
253
|
title: rec,
|
|
138
254
|
description: rec,
|
|
139
255
|
recommendation: rec,
|
|
140
|
-
});
|
|
141
|
-
|
|
256
|
+
}));
|
|
257
|
+
return {
|
|
258
|
+
key: "preload",
|
|
259
|
+
score: preload.score,
|
|
260
|
+
issues,
|
|
261
|
+
result: preload,
|
|
262
|
+
};
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
let performanceFn;
|
|
266
|
+
if (categories.includes("performance")) {
|
|
267
|
+
performanceFn = async () => {
|
|
268
|
+
const available = await isRendererAvailable();
|
|
269
|
+
if (!available) {
|
|
270
|
+
// Fall back to heuristic performance analysis.
|
|
271
|
+
const perf = calculatePerformanceFromFetch(fetchResult, fetchResult.finalUrl);
|
|
272
|
+
const weight = analyzePageWeight(fetchResult, fetchResult.finalUrl);
|
|
273
|
+
return {
|
|
274
|
+
key: "performance",
|
|
275
|
+
score: perf.score,
|
|
276
|
+
issues: [],
|
|
277
|
+
result: { performance: perf, pageWeight: weight },
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const [mobileRender, desktopRender] = await Promise.allSettled([
|
|
282
|
+
renderPage(url, config, {
|
|
283
|
+
viewport: "mobile",
|
|
284
|
+
capturePerformance: true,
|
|
285
|
+
captureA11y: false,
|
|
286
|
+
captureConsole: false,
|
|
287
|
+
blockResources: [],
|
|
288
|
+
timeout: config.timeout * 2,
|
|
289
|
+
signal: controller.signal,
|
|
290
|
+
}),
|
|
291
|
+
renderPage(url, config, {
|
|
292
|
+
viewport: "desktop",
|
|
293
|
+
capturePerformance: true,
|
|
294
|
+
captureA11y: false,
|
|
295
|
+
captureConsole: false,
|
|
296
|
+
blockResources: [],
|
|
297
|
+
timeout: config.timeout * 2,
|
|
298
|
+
signal: controller.signal,
|
|
299
|
+
}),
|
|
300
|
+
]);
|
|
301
|
+
const mobileOk = mobileRender.status === "fulfilled";
|
|
302
|
+
const desktopOk = desktopRender.status === "fulfilled";
|
|
303
|
+
if (!mobileOk && !desktopOk) {
|
|
304
|
+
// Both renders failed — fall back to heuristic.
|
|
305
|
+
const perf = calculatePerformanceFromFetch(fetchResult, fetchResult.finalUrl);
|
|
306
|
+
const weight = analyzePageWeight(fetchResult, fetchResult.finalUrl);
|
|
307
|
+
return {
|
|
308
|
+
key: "performance",
|
|
309
|
+
score: perf.score,
|
|
310
|
+
issues: [],
|
|
311
|
+
result: { performance: perf, pageWeight: weight },
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
const mobileMeasured = mobileOk ? mobileRender.value.measuredPerformance : undefined;
|
|
315
|
+
const desktopMeasured = desktopOk ? desktopRender.value.measuredPerformance : undefined;
|
|
316
|
+
// Mobile-first SEO standard: main result = mobile
|
|
317
|
+
const perf = calculatePerformanceFromFetch(fetchResult, fetchResult.finalUrl, mobileMeasured);
|
|
318
|
+
let desktopPerf;
|
|
319
|
+
if (desktopMeasured) {
|
|
320
|
+
desktopPerf = calculatePerformanceFromFetch(fetchResult, fetchResult.finalUrl, desktopMeasured);
|
|
321
|
+
perf.desktop = desktopPerf;
|
|
322
|
+
}
|
|
323
|
+
const weight = analyzePageWeight(fetchResult, fetchResult.finalUrl, mobileMeasured);
|
|
324
|
+
// Weighted score: 60% mobile, 40% desktop
|
|
325
|
+
const mobileScore = perf.score;
|
|
326
|
+
const desktopScore = desktopPerf ? desktopPerf.score : mobileScore;
|
|
327
|
+
const weightedScore = Math.round(mobileScore * 0.6 + desktopScore * 0.4);
|
|
328
|
+
perf.score = weightedScore;
|
|
329
|
+
return {
|
|
330
|
+
key: "performance",
|
|
331
|
+
score: weightedScore,
|
|
332
|
+
issues: [],
|
|
333
|
+
result: { performance: perf, pageWeight: weight },
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Fall back to heuristic on any render failure.
|
|
338
|
+
const perf = calculatePerformanceFromFetch(fetchResult, fetchResult.finalUrl);
|
|
339
|
+
const weight = analyzePageWeight(fetchResult, fetchResult.finalUrl);
|
|
340
|
+
return {
|
|
341
|
+
key: "performance",
|
|
342
|
+
score: perf.score,
|
|
343
|
+
issues: [],
|
|
344
|
+
result: { performance: perf, pageWeight: weight },
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
};
|
|
142
348
|
}
|
|
143
|
-
|
|
144
|
-
let agentUx;
|
|
349
|
+
let agentUxFn;
|
|
145
350
|
if (categories.includes("agentUx")) {
|
|
146
|
-
|
|
147
|
-
|
|
351
|
+
agentUxFn = async () => {
|
|
352
|
+
const available = await isRendererAvailable();
|
|
353
|
+
if (!available) {
|
|
354
|
+
return {
|
|
355
|
+
key: "agentUx",
|
|
356
|
+
issues: [
|
|
357
|
+
{
|
|
358
|
+
category: "Agent UX",
|
|
359
|
+
severity: "low",
|
|
360
|
+
title: "Renderer unavailable — Agent UX audit skipped",
|
|
361
|
+
description: "Headless renderer is not available; agent UX audit could not run.",
|
|
362
|
+
recommendation: "Install or enable a headless browser to run the agent UX audit.",
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
skipped: true,
|
|
366
|
+
result: { rendererAvailable: false },
|
|
367
|
+
};
|
|
368
|
+
}
|
|
148
369
|
const renderResult = await renderPage(url, config, {
|
|
149
370
|
captureA11y: true,
|
|
150
371
|
captureConsole: false,
|
|
372
|
+
signal: controller.signal,
|
|
151
373
|
});
|
|
152
|
-
|
|
153
|
-
|
|
374
|
+
const comparison = compareStaticRendered(fetchResult, renderResult);
|
|
375
|
+
const issues = [];
|
|
376
|
+
if (comparison.isSpa || comparison.contentGrew) {
|
|
377
|
+
issues.push({
|
|
378
|
+
category: "Rendering",
|
|
379
|
+
severity: comparison.isSpa ? "high" : "medium",
|
|
380
|
+
title: comparison.isSpa
|
|
381
|
+
? "Page appears to be a JavaScript-rendered SPA"
|
|
382
|
+
: "Rendered content differs significantly from static HTML",
|
|
383
|
+
description: comparison.notes.join(" ") || "Rendered DOM is significantly larger than static HTML.",
|
|
384
|
+
recommendation: "Ensure critical content is present in server-rendered HTML or use SSR/SSG for SEO-critical content.",
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
const agentUx = auditAgentUx(renderResult);
|
|
154
388
|
for (const opp of agentUx.opportunities) {
|
|
155
|
-
|
|
389
|
+
issues.push({
|
|
156
390
|
category: "Agent UX",
|
|
157
391
|
severity: "medium",
|
|
158
392
|
title: opp,
|
|
@@ -160,22 +394,73 @@ async function runFullAudit(url, config, categories) {
|
|
|
160
394
|
recommendation: opp,
|
|
161
395
|
});
|
|
162
396
|
}
|
|
397
|
+
return {
|
|
398
|
+
key: "agentUx",
|
|
399
|
+
score: agentUx.agentUxScore,
|
|
400
|
+
issues,
|
|
401
|
+
result: { agentUx, renderComparison: comparison, rendererAvailable: true },
|
|
402
|
+
};
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
const outcomes = await Promise.allSettled([...subAuditFns, ...(agentUxFn ? [agentUxFn] : []), ...(performanceFn ? [performanceFn] : [])].map((fn) => fn()));
|
|
406
|
+
clearTimeout(timeout);
|
|
407
|
+
const resultMap = new Map();
|
|
408
|
+
for (const outcome of outcomes) {
|
|
409
|
+
if (outcome.status === "fulfilled") {
|
|
410
|
+
const value = outcome.value;
|
|
411
|
+
resultMap.set(value.key, value);
|
|
412
|
+
allIssues.push(...value.issues);
|
|
413
|
+
if (typeof value.score === "number") {
|
|
414
|
+
scores.push(value.score);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
allIssues.push({
|
|
419
|
+
category: "Audit",
|
|
420
|
+
severity: "medium",
|
|
421
|
+
title: "Sub-audit failed",
|
|
422
|
+
description: `A sub-audit rejected: ${outcome.reason instanceof Error ? outcome.reason.message : String(outcome.reason)}`,
|
|
423
|
+
recommendation: "Review the failing sub-audit and re-run the full audit.",
|
|
424
|
+
});
|
|
163
425
|
}
|
|
164
426
|
}
|
|
427
|
+
const technical = resultMap.get("technical")?.result;
|
|
428
|
+
const content = resultMap.get("content")?.result;
|
|
429
|
+
const schema = resultMap.get("schema")?.result;
|
|
430
|
+
const images = resultMap.get("images")?.result;
|
|
431
|
+
const preload = resultMap.get("preload")?.result;
|
|
432
|
+
const agentUx = resultMap.get("agentUx")?.result;
|
|
433
|
+
const rendererAvailable = agentUx && "rendererAvailable" in agentUx ? agentUx.rendererAvailable : false;
|
|
434
|
+
const renderComparison = agentUx && "renderComparison" in agentUx ? agentUx.renderComparison : undefined;
|
|
435
|
+
const agentUxResult = agentUx && "agentUx" in agentUx ? agentUx.agentUx : undefined;
|
|
436
|
+
const performancePayload = resultMap.get("performance")?.result;
|
|
437
|
+
const performanceResult = performancePayload?.performance;
|
|
438
|
+
const pageWeightResult = performancePayload?.pageWeight;
|
|
165
439
|
const overallScore = scores.length > 0
|
|
166
440
|
? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length)
|
|
167
441
|
: 0;
|
|
442
|
+
const coverage = Math.round((scores.length / ALL_CATEGORIES.length) * 100);
|
|
443
|
+
const confidence = coverage === 100 ? "high" : coverage >= 50 ? "medium" : "low";
|
|
168
444
|
const summary = `SEO audit for ${fetchResult.finalUrl} — Overall score: ${overallScore}/100, ${allIssues.length} issues found.`;
|
|
169
445
|
return {
|
|
170
446
|
url: fetchResult.finalUrl,
|
|
171
447
|
auditedAt: new Date().toISOString(),
|
|
172
448
|
overallScore,
|
|
449
|
+
coverage,
|
|
450
|
+
confidence,
|
|
451
|
+
skippedCategories,
|
|
452
|
+
rendererAvailable,
|
|
453
|
+
renderComparison,
|
|
454
|
+
version: 2,
|
|
455
|
+
warnings: [],
|
|
173
456
|
technical,
|
|
174
457
|
content,
|
|
175
458
|
schema,
|
|
176
459
|
images,
|
|
177
460
|
preload,
|
|
178
|
-
agentUx,
|
|
461
|
+
agentUx: agentUxResult,
|
|
462
|
+
performance: performanceResult,
|
|
463
|
+
pageWeight: pageWeightResult,
|
|
179
464
|
allIssues,
|
|
180
465
|
summary,
|
|
181
466
|
};
|
|
@@ -189,3 +474,43 @@ function severityRank(s) {
|
|
|
189
474
|
default: return 4;
|
|
190
475
|
}
|
|
191
476
|
}
|
|
477
|
+
function compareStaticRendered(fetchResult, renderResult) {
|
|
478
|
+
const staticPage = parsePage(fetchResult.content, fetchResult.finalUrl);
|
|
479
|
+
const renderedPage = parsePage(renderResult.content, renderResult.finalUrl);
|
|
480
|
+
const staticText = extractText(fetchResult.content).split(/\s+/).filter(Boolean);
|
|
481
|
+
const renderedText = extractText(renderResult.content).split(/\s+/).filter(Boolean);
|
|
482
|
+
const textDelta = renderedText.length - staticText.length;
|
|
483
|
+
const headingDelta = renderedPage.headings.length - staticPage.headings.length;
|
|
484
|
+
const linkDelta = renderedPage.internalLinkCount + renderedPage.externalLinkCount -
|
|
485
|
+
(staticPage.internalLinkCount + staticPage.externalLinkCount);
|
|
486
|
+
const imageDelta = renderedPage.images.length - staticPage.images.length;
|
|
487
|
+
const schemaDelta = renderedPage.schemaBlocks.length - staticPage.schemaBlocks.length;
|
|
488
|
+
const notes = [];
|
|
489
|
+
if (renderResult.isSpa)
|
|
490
|
+
notes.push("Initial HTML is small relative to rendered DOM (SPA signature).");
|
|
491
|
+
if (textDelta > staticText.length * 0.25)
|
|
492
|
+
notes.push(`Rendered text grew by ${textDelta} words (${Math.round((textDelta / Math.max(1, staticText.length)) * 100)}%).`);
|
|
493
|
+
if (headingDelta > 0)
|
|
494
|
+
notes.push(`${headingDelta} new headings appeared after render.`);
|
|
495
|
+
if (linkDelta > 0)
|
|
496
|
+
notes.push(`${linkDelta} new links appeared after render.`);
|
|
497
|
+
if (imageDelta > 0)
|
|
498
|
+
notes.push(`${imageDelta} new images appeared after render.`);
|
|
499
|
+
if (schemaDelta > 0)
|
|
500
|
+
notes.push(`${schemaDelta} new JSON-LD blocks appeared after render.`);
|
|
501
|
+
if (renderResult.consoleErrors.length > 0)
|
|
502
|
+
notes.push(`${renderResult.consoleErrors.length} console errors during render.`);
|
|
503
|
+
return {
|
|
504
|
+
staticByteLength: Buffer.byteLength(fetchResult.content, "utf-8"),
|
|
505
|
+
renderedByteLength: Buffer.byteLength(renderResult.content, "utf-8"),
|
|
506
|
+
contentGrew: textDelta > staticText.length * 0.25 ||
|
|
507
|
+
(renderResult.rawContent.length > 0 && renderResult.content.length > renderResult.rawContent.length * 1.5),
|
|
508
|
+
isSpa: renderResult.isSpa,
|
|
509
|
+
textDelta,
|
|
510
|
+
headingDelta,
|
|
511
|
+
linkDelta,
|
|
512
|
+
imageDelta,
|
|
513
|
+
schemaDelta,
|
|
514
|
+
notes,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
@@ -2,12 +2,23 @@
|
|
|
2
2
|
* Helper utilities for SEO tool implementations.
|
|
3
3
|
*/
|
|
4
4
|
import type { AgentToolResult } from "../../../sdk/coding-agent/index.js";
|
|
5
|
-
/**
|
|
6
|
-
export
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
/** Structured details for tool results. */
|
|
6
|
+
export interface ToolDetails<T = unknown> {
|
|
7
|
+
isError?: boolean;
|
|
8
|
+
result?: T;
|
|
9
|
+
warnings?: string[];
|
|
10
|
+
metadata?: Record<string, unknown>;
|
|
11
|
+
}
|
|
12
|
+
/** Version of the SEO tool result schema. Increment on breaking changes. */
|
|
13
|
+
export declare const SEO_RESULT_VERSION = 2;
|
|
14
|
+
/** Create a text tool result. Accepts either legacy (text, details) or new (text, result, warnings). */
|
|
15
|
+
export declare function textResult(text: string, detailsOrResult?: Record<string, unknown> | unknown, warnings?: string[]): AgentToolResult<ToolDetails>;
|
|
16
|
+
/** Create an error tool result. Accepts either legacy (msg, details) or new (msg, result, warnings). */
|
|
17
|
+
export declare function errorResult(message: string, detailsOrResult?: Record<string, unknown> | unknown, warnings?: string[]): AgentToolResult<ToolDetails>;
|
|
9
18
|
/** Format a JSON object as pretty-printed text for tool output. */
|
|
10
19
|
export declare function jsonText(obj: unknown): string;
|
|
11
20
|
/** Truncate text to a maximum length. */
|
|
12
21
|
export declare function truncate(text: string, maxLen: number): string;
|
|
22
|
+
/** Validate and normalize a tool URL input, enforcing length limits. */
|
|
23
|
+
export declare function validateToolUrl(raw: string, maxLen?: number): string;
|
|
13
24
|
//# sourceMappingURL=helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAE1E,
|
|
1
|
+
{"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../../../../src/extensions/seo/tools/helpers.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AAE1E,2CAA2C;AAC3C,MAAM,WAAW,WAAW,CAAC,CAAC,GAAG,OAAO;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,CAAC,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,4EAA4E;AAC5E,eAAO,MAAM,kBAAkB,IAAI,CAAC;AAEpC,wGAAwG;AACxG,wBAAgB,UAAU,CACxB,IAAI,EAAE,MAAM,EACZ,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EACnD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,eAAe,CAAC,WAAW,CAAC,CAwB9B;AAED,wGAAwG;AACxG,wBAAgB,WAAW,CACzB,OAAO,EAAE,MAAM,EACf,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,EACnD,QAAQ,CAAC,EAAE,MAAM,EAAE,GAClB,eAAe,CAAC,WAAW,CAAC,CAyB9B;AAED,mEAAmE;AACnE,wBAAgB,QAAQ,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAE7C;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAG7D;AAED,wEAAwE;AACxE,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,SAAO,GAAG,MAAM,CAOlE"}
|