mcp-web-validator 1.0.0 → 1.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +91 -0
- package/README.md +149 -83
- package/SECURITY.md +35 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +444 -668
- package/dist/index.js.map +1 -1
- package/dist/network.d.ts +37 -0
- package/dist/network.js +288 -0
- package/dist/network.js.map +1 -0
- package/dist/presentation.d.ts +15 -0
- package/dist/presentation.js +347 -0
- package/dist/presentation.js.map +1 -0
- package/dist/report.d.ts +43 -0
- package/dist/report.js +143 -0
- package/dist/report.js.map +1 -0
- package/dist/screenshot.d.ts +1 -0
- package/dist/screenshot.js +150 -14
- package/dist/screenshot.js.map +1 -1
- package/dist/seo-auditor.d.ts +11 -2
- package/dist/seo-auditor.js +176 -66
- package/dist/seo-auditor.js.map +1 -1
- package/dist/version.d.ts +1 -0
- package/dist/version.js +4 -0
- package/dist/version.js.map +1 -0
- package/dist/w3c-validator.d.ts +2 -0
- package/dist/w3c-validator.js +76 -17
- package/dist/w3c-validator.js.map +1 -1
- package/package.json +18 -7
package/dist/seo-auditor.js
CHANGED
|
@@ -1,41 +1,75 @@
|
|
|
1
1
|
import * as cheerio from "cheerio";
|
|
2
|
+
import { assertPublicHttpUrl, cancelResponseBody, fetchPublicHttp, getErrorMessage, PublicUrlError, } from "./network.js";
|
|
3
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
4
|
+
export const MAX_AUDIT_ISSUES = 200;
|
|
5
|
+
export const MAX_LINKS_TO_CHECK = 25;
|
|
6
|
+
const LINK_CHECK_CONCURRENCY = 5;
|
|
7
|
+
const LINK_CHECK_TIMEOUT_MS = 5_000;
|
|
8
|
+
const LINK_CHECK_USER_AGENT = `mcp-web-validator/${PACKAGE_VERSION} (+https://digestseo.com/validator-mcp/)`;
|
|
9
|
+
function addIssue(issues, issue) {
|
|
10
|
+
if (issues.length < MAX_AUDIT_ISSUES) {
|
|
11
|
+
issues.push(issue);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function createIssueCollector() {
|
|
15
|
+
const issues = [];
|
|
16
|
+
let totalIssues = 0;
|
|
17
|
+
return {
|
|
18
|
+
issues,
|
|
19
|
+
add(issue) {
|
|
20
|
+
totalIssues += 1;
|
|
21
|
+
addIssue(issues, issue);
|
|
22
|
+
},
|
|
23
|
+
details() {
|
|
24
|
+
return {
|
|
25
|
+
issues,
|
|
26
|
+
totalIssues,
|
|
27
|
+
truncated: totalIssues > issues.length,
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function isJsonLdScriptType(type) {
|
|
33
|
+
return type?.split(";", 1)[0].trim().toLowerCase() === "application/ld+json";
|
|
34
|
+
}
|
|
2
35
|
/**
|
|
3
36
|
* Audits technical SEO and accessibility basics on HTML content using Cheerio
|
|
4
37
|
*/
|
|
5
|
-
export function
|
|
38
|
+
export function auditSeoMetadataDetailed(htmlContent) {
|
|
6
39
|
const $ = cheerio.load(htmlContent);
|
|
7
|
-
const
|
|
40
|
+
const collector = createIssueCollector();
|
|
41
|
+
const { add } = collector;
|
|
8
42
|
// --- Title Tag Audits ---
|
|
9
43
|
const titleTag = $("title");
|
|
10
44
|
if (titleTag.length === 0) {
|
|
11
|
-
|
|
45
|
+
add({
|
|
12
46
|
severity: "error",
|
|
13
47
|
category: "SEO",
|
|
14
|
-
message: "Missing <title> tag.
|
|
48
|
+
message: "Missing <title> tag. Add a concise, descriptive title to help represent the page in search results.",
|
|
15
49
|
});
|
|
16
50
|
}
|
|
17
51
|
else {
|
|
18
52
|
const titleText = titleTag.text().trim();
|
|
19
53
|
if (titleText.length === 0) {
|
|
20
|
-
|
|
54
|
+
add({
|
|
21
55
|
severity: "error",
|
|
22
56
|
category: "SEO",
|
|
23
57
|
message: "The <title> tag is empty.",
|
|
24
58
|
});
|
|
25
59
|
}
|
|
26
60
|
else if (titleText.length < 30) {
|
|
27
|
-
|
|
61
|
+
add({
|
|
28
62
|
severity: "warning",
|
|
29
63
|
category: "SEO",
|
|
30
|
-
message: `Title
|
|
64
|
+
message: `Title is ${titleText.length} characters. This is shorter than the audit's common editorial range; review whether it describes the page clearly.`,
|
|
31
65
|
element: `<title>${titleText}</title>`,
|
|
32
66
|
});
|
|
33
67
|
}
|
|
34
68
|
else if (titleText.length > 60) {
|
|
35
|
-
|
|
69
|
+
add({
|
|
36
70
|
severity: "warning",
|
|
37
71
|
category: "SEO",
|
|
38
|
-
message: `Title
|
|
72
|
+
message: `Title is ${titleText.length} characters. This is longer than the audit's common editorial range; Google title links may be shortened or rewritten depending on context and device.`,
|
|
39
73
|
element: `<title>${titleText}</title>`,
|
|
40
74
|
});
|
|
41
75
|
}
|
|
@@ -43,34 +77,34 @@ export function auditSeoMetadata(htmlContent) {
|
|
|
43
77
|
// --- Meta Description Audits ---
|
|
44
78
|
const metaDescription = $('meta[name="description"]');
|
|
45
79
|
if (metaDescription.length === 0) {
|
|
46
|
-
|
|
80
|
+
add({
|
|
47
81
|
severity: "error",
|
|
48
82
|
category: "SEO",
|
|
49
|
-
message: "Missing <meta name=\"description\">.
|
|
83
|
+
message: "Missing <meta name=\"description\">. Add a concise, accurate page summary; Google may use page content or this description to generate a snippet.",
|
|
50
84
|
});
|
|
51
85
|
}
|
|
52
86
|
else {
|
|
53
87
|
const descText = metaDescription.attr("content")?.trim() || "";
|
|
54
88
|
if (descText.length === 0) {
|
|
55
|
-
|
|
89
|
+
add({
|
|
56
90
|
severity: "error",
|
|
57
91
|
category: "SEO",
|
|
58
92
|
message: "Meta description content attribute is empty.",
|
|
59
93
|
});
|
|
60
94
|
}
|
|
61
95
|
else if (descText.length < 120) {
|
|
62
|
-
|
|
96
|
+
add({
|
|
63
97
|
severity: "warning",
|
|
64
98
|
category: "SEO",
|
|
65
|
-
message: `Meta description is
|
|
99
|
+
message: `Meta description is ${descText.length} characters. This is shorter than the audit's common editorial range; review whether it provides a useful page summary.`,
|
|
66
100
|
element: `<meta name="description" content="${descText}">`,
|
|
67
101
|
});
|
|
68
102
|
}
|
|
69
103
|
else if (descText.length > 160) {
|
|
70
|
-
|
|
104
|
+
add({
|
|
71
105
|
severity: "warning",
|
|
72
106
|
category: "SEO",
|
|
73
|
-
message: `Meta description is
|
|
107
|
+
message: `Meta description is ${descText.length} characters. This is longer than the audit's common editorial range; displayed snippets may be shortened depending on the query and device.`,
|
|
74
108
|
element: `<meta name="description" content="${descText}">`,
|
|
75
109
|
});
|
|
76
110
|
}
|
|
@@ -78,7 +112,7 @@ export function auditSeoMetadata(htmlContent) {
|
|
|
78
112
|
// --- Canonical Link ---
|
|
79
113
|
const canonical = $('link[rel="canonical"]');
|
|
80
114
|
if (canonical.length === 0) {
|
|
81
|
-
|
|
115
|
+
add({
|
|
82
116
|
severity: "warning",
|
|
83
117
|
category: "SEO",
|
|
84
118
|
message: "Missing canonical tag (<link rel=\"canonical\">). This helps prevent duplicate content issues.",
|
|
@@ -87,26 +121,26 @@ export function auditSeoMetadata(htmlContent) {
|
|
|
87
121
|
// --- Viewport Meta Tag (Mobile Responsiveness) ---
|
|
88
122
|
const viewport = $('meta[name="viewport"]');
|
|
89
123
|
if (viewport.length === 0) {
|
|
90
|
-
|
|
124
|
+
add({
|
|
91
125
|
severity: "error",
|
|
92
126
|
category: "SEO",
|
|
93
|
-
message: "Missing <meta name=\"viewport\"> tag.
|
|
127
|
+
message: "Missing <meta name=\"viewport\"> tag. Review mobile rendering; this tag helps browsers size and scale the page on mobile devices.",
|
|
94
128
|
});
|
|
95
129
|
}
|
|
96
130
|
// --- Heading Structure ---
|
|
97
131
|
const h1Tags = $("h1");
|
|
98
132
|
if (h1Tags.length === 0) {
|
|
99
|
-
|
|
100
|
-
severity: "
|
|
133
|
+
add({
|
|
134
|
+
severity: "warning",
|
|
101
135
|
category: "SEO",
|
|
102
|
-
message: "
|
|
136
|
+
message: "No <h1> heading found. Review whether the page has a clear main heading and a meaningful heading hierarchy.",
|
|
103
137
|
});
|
|
104
138
|
}
|
|
105
139
|
else if (h1Tags.length > 1) {
|
|
106
|
-
|
|
107
|
-
severity: "
|
|
140
|
+
add({
|
|
141
|
+
severity: "info",
|
|
108
142
|
category: "SEO",
|
|
109
|
-
message: `Found multiple (${h1Tags.length}) <h1>
|
|
143
|
+
message: `Found multiple (${h1Tags.length}) <h1> headings. Multiple H1s are not inherently an SEO error; ensure the heading hierarchy is meaningful and the main visual title is clear.`,
|
|
110
144
|
});
|
|
111
145
|
}
|
|
112
146
|
// --- Images Alt Tags (SEO + Accessibility) ---
|
|
@@ -115,7 +149,7 @@ export function auditSeoMetadata(htmlContent) {
|
|
|
115
149
|
const src = img.attr("src") || "unknown-source";
|
|
116
150
|
const alt = img.attr("alt");
|
|
117
151
|
if (alt === undefined) {
|
|
118
|
-
|
|
152
|
+
add({
|
|
119
153
|
severity: "error",
|
|
120
154
|
category: "Accessibility",
|
|
121
155
|
message: "Missing 'alt' attribute on image. This makes it inaccessible to screen readers.",
|
|
@@ -124,7 +158,7 @@ export function auditSeoMetadata(htmlContent) {
|
|
|
124
158
|
}
|
|
125
159
|
else if (alt.trim() === "") {
|
|
126
160
|
// Empty alt is acceptable for purely decorative images, but worth warning
|
|
127
|
-
|
|
161
|
+
add({
|
|
128
162
|
severity: "info",
|
|
129
163
|
category: "Accessibility",
|
|
130
164
|
message: "Empty 'alt' attribute found. Ensure this image is purely decorative, otherwise add descriptive text.",
|
|
@@ -136,24 +170,29 @@ export function auditSeoMetadata(htmlContent) {
|
|
|
136
170
|
const ogTitle = $('meta[property="og:title"]');
|
|
137
171
|
const ogImage = $('meta[property="og:image"]');
|
|
138
172
|
if (ogTitle.length === 0 || ogImage.length === 0) {
|
|
139
|
-
|
|
173
|
+
add({
|
|
140
174
|
severity: "info",
|
|
141
175
|
category: "SEO",
|
|
142
176
|
message: "Missing Open Graph social metadata (og:title / og:image). Add these to control preview cards on platforms like LinkedIn and X.",
|
|
143
177
|
});
|
|
144
178
|
}
|
|
145
|
-
return
|
|
179
|
+
return collector.details();
|
|
180
|
+
}
|
|
181
|
+
export function auditSeoMetadata(htmlContent) {
|
|
182
|
+
return auditSeoMetadataDetailed(htmlContent).issues;
|
|
146
183
|
}
|
|
147
184
|
/**
|
|
148
185
|
* Parses and validates JSON-LD Schema markup
|
|
149
186
|
*/
|
|
150
|
-
export function
|
|
187
|
+
export function validateSchemaMarkupDetailed(htmlContent) {
|
|
151
188
|
const $ = cheerio.load(htmlContent);
|
|
152
|
-
const
|
|
153
|
-
|
|
189
|
+
const collector = createIssueCollector();
|
|
190
|
+
const { add } = collector;
|
|
191
|
+
const blocks = $("script[type]").filter((_, element) => isJsonLdScriptType($(element).attr("type")));
|
|
192
|
+
blocks.each((index, element) => {
|
|
154
193
|
const scriptText = $(element).html() || "";
|
|
155
194
|
if (scriptText.trim() === "") {
|
|
156
|
-
|
|
195
|
+
add({
|
|
157
196
|
severity: "warning",
|
|
158
197
|
category: "Schema",
|
|
159
198
|
message: `JSON-LD block #${index + 1} is empty.`,
|
|
@@ -163,71 +202,142 @@ export function validateSchemaMarkup(htmlContent) {
|
|
|
163
202
|
try {
|
|
164
203
|
JSON.parse(scriptText);
|
|
165
204
|
}
|
|
166
|
-
catch (
|
|
167
|
-
|
|
205
|
+
catch (error) {
|
|
206
|
+
add({
|
|
168
207
|
severity: "error",
|
|
169
208
|
category: "Schema",
|
|
170
|
-
message: `Invalid JSON-LD schema syntax: ${
|
|
209
|
+
message: `Invalid JSON-LD schema syntax: ${getErrorMessage(error)}`,
|
|
171
210
|
element: `<script type="application/ld+json">...</script>`,
|
|
172
211
|
});
|
|
173
212
|
}
|
|
174
213
|
});
|
|
175
|
-
return
|
|
214
|
+
return collector.details();
|
|
215
|
+
}
|
|
216
|
+
export function validateSchemaMarkup(htmlContent) {
|
|
217
|
+
return validateSchemaMarkupDetailed(htmlContent).issues;
|
|
176
218
|
}
|
|
177
219
|
/**
|
|
178
|
-
* Extracts and tests
|
|
220
|
+
* Extracts and tests links, treating 3xx responses as reachable redirects and 4xx/5xx as broken.
|
|
179
221
|
*/
|
|
180
|
-
export async function checkBrokenLinks(htmlContent, baseUrl) {
|
|
222
|
+
export async function checkBrokenLinks(htmlContent, baseUrl, maxLinks = MAX_LINKS_TO_CHECK) {
|
|
223
|
+
if (!Number.isSafeInteger(maxLinks) || maxLinks <= 0) {
|
|
224
|
+
throw new Error("maxLinks must be a positive integer");
|
|
225
|
+
}
|
|
226
|
+
const linkLimit = Math.min(maxLinks, MAX_LINKS_TO_CHECK);
|
|
181
227
|
const $ = cheerio.load(htmlContent);
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
resolvedUrl = new URL(href, baseUrl).toString();
|
|
228
|
+
let parsedBaseUrl = baseUrl ? await assertPublicHttpUrl(baseUrl) : undefined;
|
|
229
|
+
if (!parsedBaseUrl) {
|
|
230
|
+
const documentBaseHref = $("base[href]").first().attr("href")?.trim();
|
|
231
|
+
if (documentBaseHref) {
|
|
232
|
+
try {
|
|
233
|
+
parsedBaseUrl = await assertPublicHttpUrl(documentBaseHref);
|
|
189
234
|
}
|
|
190
|
-
|
|
191
|
-
|
|
235
|
+
catch (error) {
|
|
236
|
+
if (!(error instanceof PublicUrlError)) {
|
|
237
|
+
throw error;
|
|
238
|
+
}
|
|
192
239
|
}
|
|
193
240
|
}
|
|
241
|
+
}
|
|
242
|
+
const urls = [];
|
|
243
|
+
const seenUrls = new Set();
|
|
244
|
+
$("a").each((_, element) => {
|
|
245
|
+
if (urls.length >= linkLimit) {
|
|
246
|
+
return false;
|
|
247
|
+
}
|
|
248
|
+
const href = $(element).attr("href")?.trim();
|
|
249
|
+
if (!href || href.startsWith("#")) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
let resolvedUrl;
|
|
253
|
+
try {
|
|
254
|
+
resolvedUrl = parsedBaseUrl ? new URL(href, parsedBaseUrl) : new URL(href);
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
// Relative links require a public base URL; unsupported or malformed links are skipped.
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
if (resolvedUrl.protocol !== "http:" && resolvedUrl.protocol !== "https:") {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
resolvedUrl.hash = "";
|
|
264
|
+
const normalizedUrl = resolvedUrl.href;
|
|
265
|
+
if (!seenUrls.has(normalizedUrl)) {
|
|
266
|
+
seenUrls.add(normalizedUrl);
|
|
267
|
+
urls.push(normalizedUrl);
|
|
268
|
+
}
|
|
194
269
|
});
|
|
195
|
-
const results =
|
|
196
|
-
|
|
197
|
-
|
|
270
|
+
const results = new Array(urls.length);
|
|
271
|
+
let nextIndex = 0;
|
|
272
|
+
async function checkLink(url) {
|
|
198
273
|
try {
|
|
199
|
-
const
|
|
274
|
+
const headResult = await fetchPublicHttp(url, {
|
|
200
275
|
method: "HEAD",
|
|
201
|
-
|
|
202
|
-
|
|
276
|
+
headers: { "User-Agent": LINK_CHECK_USER_AGENT },
|
|
277
|
+
timeoutMs: LINK_CHECK_TIMEOUT_MS,
|
|
278
|
+
maxRedirects: 0,
|
|
203
279
|
});
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
280
|
+
const headStatus = headResult.response.status;
|
|
281
|
+
const headFinalUrl = headResult.url.href;
|
|
282
|
+
await cancelResponseBody(headResult.response);
|
|
283
|
+
// Some sites reject or do not implement HEAD even when the linked resource is available.
|
|
284
|
+
if (headStatus === 405 || headStatus === 403 || headStatus === 501) {
|
|
285
|
+
const getResult = await fetchPublicHttp(url, {
|
|
207
286
|
method: "GET",
|
|
208
|
-
|
|
287
|
+
headers: {
|
|
288
|
+
Range: "bytes=0-0",
|
|
289
|
+
"User-Agent": LINK_CHECK_USER_AGENT,
|
|
290
|
+
},
|
|
291
|
+
timeoutMs: LINK_CHECK_TIMEOUT_MS,
|
|
292
|
+
maxRedirects: 0,
|
|
209
293
|
});
|
|
294
|
+
const status = getResult.response.status;
|
|
295
|
+
const ok = status >= 200 && status < 400;
|
|
296
|
+
const finalUrl = getResult.url.href;
|
|
297
|
+
await cancelResponseBody(getResult.response);
|
|
210
298
|
return {
|
|
211
299
|
url,
|
|
212
|
-
status
|
|
213
|
-
ok
|
|
300
|
+
status,
|
|
301
|
+
ok,
|
|
302
|
+
message: status >= 300 && status < 400
|
|
303
|
+
? "Redirect not followed"
|
|
304
|
+
: finalUrl !== url
|
|
305
|
+
? `Redirected to ${finalUrl}`
|
|
306
|
+
: undefined,
|
|
214
307
|
};
|
|
215
308
|
}
|
|
216
309
|
return {
|
|
217
310
|
url,
|
|
218
|
-
status:
|
|
219
|
-
ok:
|
|
311
|
+
status: headStatus,
|
|
312
|
+
ok: headStatus >= 200 && headStatus < 400,
|
|
313
|
+
message: headStatus >= 300 && headStatus < 400
|
|
314
|
+
? "Redirect not followed"
|
|
315
|
+
: headFinalUrl !== url
|
|
316
|
+
? `Redirected to ${headFinalUrl}`
|
|
317
|
+
: undefined,
|
|
220
318
|
};
|
|
221
319
|
}
|
|
222
320
|
catch (error) {
|
|
223
321
|
return {
|
|
224
322
|
url,
|
|
225
|
-
status: "
|
|
323
|
+
status: error instanceof PublicUrlError ? "blocked" : "failed",
|
|
226
324
|
ok: false,
|
|
227
|
-
message: error
|
|
325
|
+
message: getErrorMessage(error),
|
|
228
326
|
};
|
|
229
327
|
}
|
|
230
|
-
}
|
|
231
|
-
|
|
328
|
+
}
|
|
329
|
+
async function worker() {
|
|
330
|
+
while (true) {
|
|
331
|
+
const index = nextIndex;
|
|
332
|
+
nextIndex += 1;
|
|
333
|
+
if (index >= urls.length) {
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
results[index] = await checkLink(urls[index]);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const workerCount = Math.min(LINK_CHECK_CONCURRENCY, urls.length);
|
|
340
|
+
await Promise.all(Array.from({ length: workerCount }, () => worker()));
|
|
341
|
+
return results;
|
|
232
342
|
}
|
|
233
343
|
//# sourceMappingURL=seo-auditor.js.map
|
package/dist/seo-auditor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"seo-auditor.js","sourceRoot":"","sources":["../src/seo-auditor.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"seo-auditor.js","sourceRoot":"","sources":["../src/seo-auditor.ts"],"names":[],"mappings":"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","sourcesContent":["import * as cheerio from \"cheerio\";\nimport {\n assertPublicHttpUrl,\n cancelResponseBody,\n fetchPublicHttp,\n getErrorMessage,\n PublicUrlError,\n} from \"./network.js\";\nimport { PACKAGE_VERSION } from \"./version.js\";\n\nexport const MAX_AUDIT_ISSUES = 200;\nexport const MAX_LINKS_TO_CHECK = 25;\nconst LINK_CHECK_CONCURRENCY = 5;\nconst LINK_CHECK_TIMEOUT_MS = 5_000;\nconst LINK_CHECK_USER_AGENT = `mcp-web-validator/${PACKAGE_VERSION} (+https://digestseo.com/validator-mcp/)`;\n\nexport interface SEOIssue {\n severity: \"error\" | \"warning\" | \"info\";\n category: \"SEO\" | \"Schema\" | \"BrokenLinks\" | \"Accessibility\";\n message: string;\n element?: string;\n}\n\nexport interface LinkStatus {\n url: string;\n status: number | string;\n ok: boolean;\n message?: string;\n}\n\nexport interface AuditDetails {\n issues: SEOIssue[];\n totalIssues: number;\n truncated: boolean;\n}\n\nfunction addIssue(issues: SEOIssue[], issue: SEOIssue): void {\n if (issues.length < MAX_AUDIT_ISSUES) {\n issues.push(issue);\n }\n}\n\nfunction createIssueCollector(): {\n issues: SEOIssue[];\n add: (issue: SEOIssue) => void;\n details: () => AuditDetails;\n} {\n const issues: SEOIssue[] = [];\n let totalIssues = 0;\n return {\n issues,\n add(issue) {\n totalIssues += 1;\n addIssue(issues, issue);\n },\n details() {\n return {\n issues,\n totalIssues,\n truncated: totalIssues > issues.length,\n };\n },\n };\n}\n\nfunction isJsonLdScriptType(type: string | undefined): boolean {\n return type?.split(\";\", 1)[0].trim().toLowerCase() === \"application/ld+json\";\n}\n\n/**\n * Audits technical SEO and accessibility basics on HTML content using Cheerio\n */\nexport function auditSeoMetadataDetailed(htmlContent: string): AuditDetails {\n const $ = cheerio.load(htmlContent);\n const collector = createIssueCollector();\n const { add } = collector;\n\n // --- Title Tag Audits ---\n const titleTag = $(\"title\");\n if (titleTag.length === 0) {\n add({\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <title> tag. Add a concise, descriptive title to help represent the page in search results.\",\n });\n } else {\n const titleText = titleTag.text().trim();\n if (titleText.length === 0) {\n add({\n severity: \"error\",\n category: \"SEO\",\n message: \"The <title> tag is empty.\",\n });\n } else if (titleText.length < 30) {\n add({\n severity: \"warning\",\n category: \"SEO\",\n message: `Title is ${titleText.length} characters. This is shorter than the audit's common editorial range; review whether it describes the page clearly.`,\n element: `<title>${titleText}</title>`,\n });\n } else if (titleText.length > 60) {\n add({\n severity: \"warning\",\n category: \"SEO\",\n message: `Title is ${titleText.length} characters. This is longer than the audit's common editorial range; Google title links may be shortened or rewritten depending on context and device.`,\n element: `<title>${titleText}</title>`,\n });\n }\n }\n\n // --- Meta Description Audits ---\n const metaDescription = $('meta[name=\"description\"]');\n if (metaDescription.length === 0) {\n add({\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <meta name=\\\"description\\\">. Add a concise, accurate page summary; Google may use page content or this description to generate a snippet.\",\n });\n } else {\n const descText = metaDescription.attr(\"content\")?.trim() || \"\";\n if (descText.length === 0) {\n add({\n severity: \"error\",\n category: \"SEO\",\n message: \"Meta description content attribute is empty.\",\n });\n } else if (descText.length < 120) {\n add({\n severity: \"warning\",\n category: \"SEO\",\n message: `Meta description is ${descText.length} characters. This is shorter than the audit's common editorial range; review whether it provides a useful page summary.`,\n element: `<meta name=\"description\" content=\"${descText}\">`,\n });\n } else if (descText.length > 160) {\n add({\n severity: \"warning\",\n category: \"SEO\",\n message: `Meta description is ${descText.length} characters. This is longer than the audit's common editorial range; displayed snippets may be shortened depending on the query and device.`,\n element: `<meta name=\"description\" content=\"${descText}\">`,\n });\n }\n }\n\n // --- Canonical Link ---\n const canonical = $('link[rel=\"canonical\"]');\n if (canonical.length === 0) {\n add({\n severity: \"warning\",\n category: \"SEO\",\n message: \"Missing canonical tag (<link rel=\\\"canonical\\\">). This helps prevent duplicate content issues.\",\n });\n }\n\n // --- Viewport Meta Tag (Mobile Responsiveness) ---\n const viewport = $('meta[name=\"viewport\"]');\n if (viewport.length === 0) {\n add({\n severity: \"error\",\n category: \"SEO\",\n message: \"Missing <meta name=\\\"viewport\\\"> tag. Review mobile rendering; this tag helps browsers size and scale the page on mobile devices.\",\n });\n }\n\n // --- Heading Structure ---\n const h1Tags = $(\"h1\");\n if (h1Tags.length === 0) {\n add({\n severity: \"warning\",\n category: \"SEO\",\n message: \"No <h1> heading found. Review whether the page has a clear main heading and a meaningful heading hierarchy.\",\n });\n } else if (h1Tags.length > 1) {\n add({\n severity: \"info\",\n category: \"SEO\",\n message: `Found multiple (${h1Tags.length}) <h1> headings. Multiple H1s are not inherently an SEO error; ensure the heading hierarchy is meaningful and the main visual title is clear.`,\n });\n }\n\n // --- Images Alt Tags (SEO + Accessibility) ---\n $(\"img\").each((_, element) => {\n const img = $(element);\n const src = img.attr(\"src\") || \"unknown-source\";\n const alt = img.attr(\"alt\");\n\n if (alt === undefined) {\n add({\n severity: \"error\",\n category: \"Accessibility\",\n message: \"Missing 'alt' attribute on image. This makes it inaccessible to screen readers.\",\n element: `<img src=\"${src}\">`,\n });\n } else if (alt.trim() === \"\") {\n // Empty alt is acceptable for purely decorative images, but worth warning\n add({\n severity: \"info\",\n category: \"Accessibility\",\n message: \"Empty 'alt' attribute found. Ensure this image is purely decorative, otherwise add descriptive text.\",\n element: `<img src=\"${src}\" alt=\"\">`,\n });\n }\n });\n\n // --- Open Graph / Social Tags ---\n const ogTitle = $('meta[property=\"og:title\"]');\n const ogImage = $('meta[property=\"og:image\"]');\n if (ogTitle.length === 0 || ogImage.length === 0) {\n add({\n severity: \"info\",\n category: \"SEO\",\n message: \"Missing Open Graph social metadata (og:title / og:image). Add these to control preview cards on platforms like LinkedIn and X.\",\n });\n }\n\n return collector.details();\n}\n\nexport function auditSeoMetadata(htmlContent: string): SEOIssue[] {\n return auditSeoMetadataDetailed(htmlContent).issues;\n}\n\n/**\n * Parses and validates JSON-LD Schema markup\n */\nexport function validateSchemaMarkupDetailed(htmlContent: string): AuditDetails {\n const $ = cheerio.load(htmlContent);\n const collector = createIssueCollector();\n const { add } = collector;\n const blocks = $(\"script[type]\").filter((_, element) => isJsonLdScriptType($(element).attr(\"type\")));\n\n blocks.each((index, element) => {\n const scriptText = $(element).html() || \"\";\n if (scriptText.trim() === \"\") {\n add({\n severity: \"warning\",\n category: \"Schema\",\n message: `JSON-LD block #${index + 1} is empty.`,\n });\n return;\n }\n\n try {\n JSON.parse(scriptText);\n } catch (error: unknown) {\n add({\n severity: \"error\",\n category: \"Schema\",\n message: `Invalid JSON-LD schema syntax: ${getErrorMessage(error)}`,\n element: `<script type=\"application/ld+json\">...</script>`,\n });\n }\n });\n\n return collector.details();\n}\n\nexport function validateSchemaMarkup(htmlContent: string): SEOIssue[] {\n return validateSchemaMarkupDetailed(htmlContent).issues;\n}\n\n/**\n * Extracts and tests links, treating 3xx responses as reachable redirects and 4xx/5xx as broken.\n */\nexport async function checkBrokenLinks(\n htmlContent: string,\n baseUrl?: string,\n maxLinks = MAX_LINKS_TO_CHECK,\n): Promise<LinkStatus[]> {\n if (!Number.isSafeInteger(maxLinks) || maxLinks <= 0) {\n throw new Error(\"maxLinks must be a positive integer\");\n }\n const linkLimit = Math.min(maxLinks, MAX_LINKS_TO_CHECK);\n const $ = cheerio.load(htmlContent);\n let parsedBaseUrl = baseUrl ? await assertPublicHttpUrl(baseUrl) : undefined;\n if (!parsedBaseUrl) {\n const documentBaseHref = $(\"base[href]\").first().attr(\"href\")?.trim();\n if (documentBaseHref) {\n try {\n parsedBaseUrl = await assertPublicHttpUrl(documentBaseHref);\n } catch (error: unknown) {\n if (!(error instanceof PublicUrlError)) {\n throw error;\n }\n }\n }\n }\n const urls: string[] = [];\n const seenUrls = new Set<string>();\n\n $(\"a\").each((_, element) => {\n if (urls.length >= linkLimit) {\n return false;\n }\n\n const href = $(element).attr(\"href\")?.trim();\n if (!href || href.startsWith(\"#\")) {\n return;\n }\n\n let resolvedUrl: URL;\n try {\n resolvedUrl = parsedBaseUrl ? new URL(href, parsedBaseUrl) : new URL(href);\n } catch {\n // Relative links require a public base URL; unsupported or malformed links are skipped.\n return;\n }\n\n if (resolvedUrl.protocol !== \"http:\" && resolvedUrl.protocol !== \"https:\") {\n return;\n }\n\n resolvedUrl.hash = \"\";\n const normalizedUrl = resolvedUrl.href;\n if (!seenUrls.has(normalizedUrl)) {\n seenUrls.add(normalizedUrl);\n urls.push(normalizedUrl);\n }\n });\n\n const results = new Array<LinkStatus>(urls.length);\n let nextIndex = 0;\n\n async function checkLink(url: string): Promise<LinkStatus> {\n try {\n const headResult = await fetchPublicHttp(url, {\n method: \"HEAD\",\n headers: { \"User-Agent\": LINK_CHECK_USER_AGENT },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n const headStatus = headResult.response.status;\n const headFinalUrl = headResult.url.href;\n await cancelResponseBody(headResult.response);\n\n // Some sites reject or do not implement HEAD even when the linked resource is available.\n if (headStatus === 405 || headStatus === 403 || headStatus === 501) {\n const getResult = await fetchPublicHttp(url, {\n method: \"GET\",\n headers: {\n Range: \"bytes=0-0\",\n \"User-Agent\": LINK_CHECK_USER_AGENT,\n },\n timeoutMs: LINK_CHECK_TIMEOUT_MS,\n maxRedirects: 0,\n });\n const status = getResult.response.status;\n const ok = status >= 200 && status < 400;\n const finalUrl = getResult.url.href;\n await cancelResponseBody(getResult.response);\n return {\n url,\n status,\n ok,\n message:\n status >= 300 && status < 400\n ? \"Redirect not followed\"\n : finalUrl !== url\n ? `Redirected to ${finalUrl}`\n : undefined,\n };\n }\n\n return {\n url,\n status: headStatus,\n ok: headStatus >= 200 && headStatus < 400,\n message:\n headStatus >= 300 && headStatus < 400\n ? \"Redirect not followed\"\n : headFinalUrl !== url\n ? `Redirected to ${headFinalUrl}`\n : undefined,\n };\n } catch (error: unknown) {\n return {\n url,\n status: error instanceof PublicUrlError ? \"blocked\" : \"failed\",\n ok: false,\n message: getErrorMessage(error),\n };\n }\n }\n\n async function worker(): Promise<void> {\n while (true) {\n const index = nextIndex;\n nextIndex += 1;\n if (index >= urls.length) {\n return;\n }\n results[index] = await checkLink(urls[index]);\n }\n }\n\n const workerCount = Math.min(LINK_CHECK_CONCURRENCY, urls.length);\n await Promise.all(Array.from({ length: workerCount }, () => worker()));\n return results;\n}\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const PACKAGE_VERSION: string;
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5C,MAAM,WAAW,GAAG,aAAa,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,iBAAiB,CAAwB,CAAC;AAE7F,MAAM,CAAC,MAAM,eAAe,GAAG,WAAW,CAAC,OAAO,CAAC","sourcesContent":["import { createRequire } from \"node:module\";\n\nconst packageJson = createRequire(import.meta.url)(\"../package.json\") as { version: string };\n\nexport const PACKAGE_VERSION = packageJson.version;\n"]}
|
package/dist/w3c-validator.d.ts
CHANGED
package/dist/w3c-validator.js
CHANGED
|
@@ -1,25 +1,75 @@
|
|
|
1
|
+
import { cancelResponseBody, getErrorMessage, readResponseText, } from "./network.js";
|
|
2
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
3
|
+
const VALIDATOR_TIMEOUT_MS = 20_000;
|
|
4
|
+
const MAX_HTML_BYTES = 2_000_000;
|
|
5
|
+
export const MAX_CSS_VALIDATION_BYTES = 128_000;
|
|
6
|
+
const MAX_VALIDATOR_RESPONSE_BYTES = 5_000_000;
|
|
7
|
+
const USER_AGENT = `mcp-web-validator/${PACKAGE_VERSION} (+https://digestseo.com/validator-mcp/)`;
|
|
8
|
+
export const MAX_VALIDATION_MESSAGES = 200;
|
|
9
|
+
function assertContentSize(content, maxBytes, label) {
|
|
10
|
+
const size = Buffer.byteLength(content, "utf8");
|
|
11
|
+
if (size > maxBytes) {
|
|
12
|
+
throw new Error(`${label} exceeds the ${maxBytes}-byte validation limit`);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function normalizeW3CMessage(message) {
|
|
16
|
+
if (typeof message !== "object" || message === null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
const candidate = message;
|
|
20
|
+
if (typeof candidate.type !== "string" || typeof candidate.message !== "string") {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
const normalized = {
|
|
24
|
+
type: candidate.type,
|
|
25
|
+
message: candidate.message,
|
|
26
|
+
};
|
|
27
|
+
for (const field of ["lastLine", "lastColumn", "firstLine", "firstColumn"]) {
|
|
28
|
+
if (typeof candidate[field] === "number" && Number.isInteger(candidate[field])) {
|
|
29
|
+
normalized[field] = candidate[field];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
if (typeof candidate.extract === "string") {
|
|
33
|
+
normalized.extract = candidate.extract;
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
}
|
|
1
37
|
/**
|
|
2
38
|
* Validates HTML using the W3C Nu HTML Checker API
|
|
3
39
|
*/
|
|
4
40
|
export async function validateHtmlContent(htmlContent) {
|
|
5
41
|
const url = "https://validator.w3.org/nu/?out=json";
|
|
6
42
|
try {
|
|
43
|
+
assertContentSize(htmlContent, MAX_HTML_BYTES, "HTML content");
|
|
7
44
|
const response = await fetch(url, {
|
|
8
45
|
method: "POST",
|
|
9
46
|
headers: {
|
|
10
47
|
"Content-Type": "text/html; charset=utf-8",
|
|
11
|
-
"User-Agent":
|
|
48
|
+
"User-Agent": USER_AGENT,
|
|
12
49
|
},
|
|
13
50
|
body: htmlContent,
|
|
51
|
+
redirect: "error",
|
|
52
|
+
signal: AbortSignal.timeout(VALIDATOR_TIMEOUT_MS),
|
|
14
53
|
});
|
|
15
54
|
if (!response.ok) {
|
|
55
|
+
await cancelResponseBody(response);
|
|
16
56
|
throw new Error(`W3C HTML validator returned HTTP status ${response.status}`);
|
|
17
57
|
}
|
|
18
|
-
const
|
|
19
|
-
|
|
58
|
+
const text = await readResponseText(response, MAX_VALIDATOR_RESPONSE_BYTES);
|
|
59
|
+
const data = JSON.parse(text);
|
|
60
|
+
if (data.messages === undefined) {
|
|
61
|
+
throw new Error("W3C HTML validator returned an invalid response shape");
|
|
62
|
+
}
|
|
63
|
+
if (!Array.isArray(data.messages)) {
|
|
64
|
+
throw new Error("W3C HTML validator returned an invalid response shape");
|
|
65
|
+
}
|
|
66
|
+
return data.messages
|
|
67
|
+
.map(normalizeW3CMessage)
|
|
68
|
+
.filter((message) => message !== null)
|
|
69
|
+
.slice(0, MAX_VALIDATION_MESSAGES);
|
|
20
70
|
}
|
|
21
71
|
catch (error) {
|
|
22
|
-
throw new Error(`
|
|
72
|
+
throw new Error(`HTML validation failed: ${getErrorMessage(error)}`);
|
|
23
73
|
}
|
|
24
74
|
}
|
|
25
75
|
/**
|
|
@@ -28,27 +78,36 @@ export async function validateHtmlContent(htmlContent) {
|
|
|
28
78
|
export async function validateCssContent(cssContent) {
|
|
29
79
|
const url = "https://jigsaw.w3.org/css-validator/validator";
|
|
30
80
|
try {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
81
|
+
assertContentSize(cssContent, MAX_CSS_VALIDATION_BYTES, "CSS content");
|
|
82
|
+
const form = new FormData();
|
|
83
|
+
form.set("text", cssContent);
|
|
84
|
+
form.set("output", "json");
|
|
85
|
+
form.set("warning", "0");
|
|
86
|
+
form.set("profile", "css3svg");
|
|
87
|
+
const response = await fetch(url, {
|
|
88
|
+
method: "POST",
|
|
37
89
|
headers: {
|
|
38
|
-
"User-Agent":
|
|
90
|
+
"User-Agent": USER_AGENT,
|
|
39
91
|
},
|
|
92
|
+
body: form,
|
|
93
|
+
redirect: "error",
|
|
94
|
+
signal: AbortSignal.timeout(VALIDATOR_TIMEOUT_MS),
|
|
40
95
|
});
|
|
41
96
|
if (!response.ok) {
|
|
97
|
+
await cancelResponseBody(response);
|
|
42
98
|
throw new Error(`W3C CSS validator returned HTTP status ${response.status}`);
|
|
43
99
|
}
|
|
44
|
-
const text = await response
|
|
45
|
-
//
|
|
100
|
+
const text = await readResponseText(response, MAX_VALIDATOR_RESPONSE_BYTES);
|
|
101
|
+
// An empty upstream response is indeterminate and must never be presented as a clean result.
|
|
46
102
|
if (!text || text.trim() === "") {
|
|
47
|
-
|
|
103
|
+
throw new Error("W3C CSS validator returned an empty response");
|
|
48
104
|
}
|
|
49
105
|
const data = JSON.parse(text);
|
|
50
|
-
|
|
51
|
-
|
|
106
|
+
if (!data.cssvalidation || typeof data.cssvalidation !== "object") {
|
|
107
|
+
throw new Error("W3C CSS validator returned an invalid response shape");
|
|
108
|
+
}
|
|
109
|
+
const errors = data.cssvalidation.errors || [];
|
|
110
|
+
return errors.slice(0, MAX_VALIDATION_MESSAGES).map(err => ({
|
|
52
111
|
line: err.line || 0,
|
|
53
112
|
type: "error",
|
|
54
113
|
message: err.message ? err.message.trim() : "Unknown CSS validation error",
|
|
@@ -56,7 +115,7 @@ export async function validateCssContent(cssContent) {
|
|
|
56
115
|
}));
|
|
57
116
|
}
|
|
58
117
|
catch (error) {
|
|
59
|
-
throw new Error(`
|
|
118
|
+
throw new Error(`CSS validation failed: ${getErrorMessage(error)}`);
|
|
60
119
|
}
|
|
61
120
|
}
|
|
62
121
|
//# sourceMappingURL=w3c-validator.js.map
|