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.
@@ -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 auditSeoMetadata(htmlContent) {
38
+ export function auditSeoMetadataDetailed(htmlContent) {
6
39
  const $ = cheerio.load(htmlContent);
7
- const issues = [];
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
- issues.push({
45
+ add({
12
46
  severity: "error",
13
47
  category: "SEO",
14
- message: "Missing <title> tag. This is critical for search indexing and click-through rates.",
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
- issues.push({
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
- issues.push({
61
+ add({
28
62
  severity: "warning",
29
63
  category: "SEO",
30
- message: `Title length (${titleText.length} chars) is too short. Try to make it at least 30-50 characters.`,
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
- issues.push({
69
+ add({
36
70
  severity: "warning",
37
71
  category: "SEO",
38
- message: `Title length (${titleText.length} chars) is too long. Search engines will truncate it. Keep it under 60 characters.`,
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
- issues.push({
80
+ add({
47
81
  severity: "error",
48
82
  category: "SEO",
49
- message: "Missing <meta name=\"description\">. Search engines will automatically generate snippets, which may lower CTR.",
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
- issues.push({
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
- issues.push({
96
+ add({
63
97
  severity: "warning",
64
98
  category: "SEO",
65
- message: `Meta description is too short (${descText.length} chars). Aim for 120-160 characters to optimize your search snippet.`,
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
- issues.push({
104
+ add({
71
105
  severity: "warning",
72
106
  category: "SEO",
73
- message: `Meta description is too long (${descText.length} chars). Search engines will truncate it. Keep it under 160 characters.`,
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
- issues.push({
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
- issues.push({
124
+ add({
91
125
  severity: "error",
92
126
  category: "SEO",
93
- message: "Missing <meta name=\"viewport\"> tag. Mobile friendliness is a critical ranking factor.",
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
- issues.push({
100
- severity: "error",
133
+ add({
134
+ severity: "warning",
101
135
  category: "SEO",
102
- message: "Missing <h1> tag. Every page must have exactly one <h1> representing the main topic.",
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
- issues.push({
107
- severity: "warning",
140
+ add({
141
+ severity: "info",
108
142
  category: "SEO",
109
- message: `Found multiple (${h1Tags.length}) <h1> tags. Multiple <h1> tags dilutes topic keyword focus.`,
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
- issues.push({
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
- issues.push({
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
- issues.push({
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 issues;
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 validateSchemaMarkup(htmlContent) {
187
+ export function validateSchemaMarkupDetailed(htmlContent) {
151
188
  const $ = cheerio.load(htmlContent);
152
- const issues = [];
153
- $('script[type="application/ld+json"]').each((index, element) => {
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
- issues.push({
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 (e) {
167
- issues.push({
205
+ catch (error) {
206
+ add({
168
207
  severity: "error",
169
208
  category: "Schema",
170
- message: `Invalid JSON-LD schema syntax: ${e.message}`,
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 issues;
214
+ return collector.details();
215
+ }
216
+ export function validateSchemaMarkup(htmlContent) {
217
+ return validateSchemaMarkupDetailed(htmlContent).issues;
176
218
  }
177
219
  /**
178
- * Extracts and tests all links inside the HTML content for broken links (4xx / 5xx)
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
- const urls = [];
183
- $("a").each((_, element) => {
184
- const href = $(element).attr("href");
185
- if (href && !href.startsWith("#") && !href.startsWith("mailto:") && !href.startsWith("tel:") && !href.startsWith("javascript:")) {
186
- let resolvedUrl = href;
187
- if (href.startsWith("/") && baseUrl) {
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
- if (!urls.includes(resolvedUrl)) {
191
- urls.push(resolvedUrl);
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
- // Parallel requests with timeout limits to keep validation fast
197
- const requests = urls.map(async (url) => {
270
+ const results = new Array(urls.length);
271
+ let nextIndex = 0;
272
+ async function checkLink(url) {
198
273
  try {
199
- const response = await fetch(url, {
274
+ const headResult = await fetchPublicHttp(url, {
200
275
  method: "HEAD",
201
- // Avoid rejection on self-signed certs or SSL errors commonly found on local testing servers
202
- signal: AbortSignal.timeout(5000),
276
+ headers: { "User-Agent": LINK_CHECK_USER_AGENT },
277
+ timeoutMs: LINK_CHECK_TIMEOUT_MS,
278
+ maxRedirects: 0,
203
279
  });
204
- // If HEAD is not allowed (e.g. Cloudflare / 405 Method Not Allowed), retry with GET
205
- if (response.status === 405 || response.status === 403) {
206
- const getResponse = await fetch(url, {
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
- signal: AbortSignal.timeout(5000),
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: getResponse.status,
213
- ok: getResponse.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: response.status,
219
- ok: response.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: "FAILED",
323
+ status: error instanceof PublicUrlError ? "blocked" : "failed",
226
324
  ok: false,
227
- message: error.name === "TimeoutError" ? "Timeout" : error.message,
325
+ message: getErrorMessage(error),
228
326
  };
229
327
  }
230
- });
231
- return Promise.all(requests);
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
@@ -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;
@@ -0,0 +1,4 @@
1
+ import { createRequire } from "node:module";
2
+ const packageJson = createRequire(import.meta.url)("../package.json");
3
+ export const PACKAGE_VERSION = packageJson.version;
4
+ //# sourceMappingURL=version.js.map
@@ -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"]}
@@ -13,6 +13,8 @@ export interface CSSMessage {
13
13
  message: string;
14
14
  context?: string;
15
15
  }
16
+ export declare const MAX_CSS_VALIDATION_BYTES = 128000;
17
+ export declare const MAX_VALIDATION_MESSAGES = 200;
16
18
  /**
17
19
  * Validates HTML using the W3C Nu HTML Checker API
18
20
  */
@@ -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": "Mozilla/5.0 (MCP Web Validator)",
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 data = (await response.json());
19
- return data.messages || [];
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(`Failed to contact W3C HTML Validator: ${error.message}`);
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
- const params = new URLSearchParams();
32
- params.append("text", cssContent);
33
- params.append("output", "json");
34
- params.append("warning", "0"); // Hide warnings to focus on errors
35
- params.append("profile", "css3svg");
36
- const response = await fetch(`${url}?${params.toString()}`, {
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": "Mozilla/5.0 (MCP Web Validator)",
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.text();
45
- // W3C Jigsaw API sometimes returns invalid JSON or empty responses if there are network issues
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
- return [];
103
+ throw new Error("W3C CSS validator returned an empty response");
48
104
  }
49
105
  const data = JSON.parse(text);
50
- const errors = data.cssvalidation?.errors || [];
51
- return errors.map(err => ({
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(`Failed to contact W3C CSS Validator: ${error.message}`);
118
+ throw new Error(`CSS validation failed: ${getErrorMessage(error)}`);
60
119
  }
61
120
  }
62
121
  //# sourceMappingURL=w3c-validator.js.map