mcp-web-validator 1.3.3 → 1.3.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,22 @@ All notable changes to this project are documented here. The project follows [Se
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [1.3.5] - 2026-09-20
8
+
9
+ ### Fixed
10
+
11
+ - Rejected malformed Nu HTML Checker message objects instead of silently dropping them from local validation results.
12
+ - Withheld the validation report SEO score when JSON-LD/schema analysis is unavailable, preventing partial reports from treating missing schema evidence as clean.
13
+
14
+ ## [1.3.4] - 2026-09-20
15
+
16
+ ### Fixed
17
+
18
+ - Rejected mismatched explicit HTTP/HTTPS port pairs in the local public-URL boundary while preserving normalized default ports.
19
+ - Matched the standard `meta name="description"` keyword ASCII case-insensitively in both local and hosted SEO audits, preventing valid mixed-case metadata from being reported missing.
20
+ - Matched hosted robots.txt crawler groups to the exact case-insensitive product token instead of accepting prefix-only groups.
21
+ - Normalized robots.txt percent encoding according to RFC 9309 comparison rules, including unreserved-octet decoding while preserving reserved escapes.
22
+
7
23
  ## [1.3.3] - 2026-09-20
8
24
 
9
25
  ### Fixed
package/dist/network.js CHANGED
@@ -129,8 +129,10 @@ export async function assertPublicHttpUrl(input) {
129
129
  if (parsed.username || parsed.password) {
130
130
  throw new PublicUrlError("URLs containing credentials are not allowed");
131
131
  }
132
- if (parsed.port && parsed.port !== "80" && parsed.port !== "443") {
133
- throw new PublicUrlError("Only standard HTTP ports 80 and 443 are allowed");
132
+ // URL normalizes matching defaults (HTTP :80 / HTTPS :443) to an empty port.
133
+ // Any remaining port is custom or mismatched to the scheme and must be rejected.
134
+ if (parsed.port) {
135
+ throw new PublicUrlError("Only default HTTP and HTTPS ports are allowed");
134
136
  }
135
137
  parsed.hash = "";
136
138
  const hostname = normalizeHostname(parsed.hostname);
@@ -1 +1 @@
1
- {"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"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","sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport * as fs from \"node:fs/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport * as path from \"node:path\";\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst DNS_TIMEOUT_MS = 5_000;\nconst MAX_URL_LENGTH = 8_192;\n\nconst blockedAddresses = new BlockList();\nfor (const [network, prefix] of [\n [\"0.0.0.0\", 8],\n [\"10.0.0.0\", 8],\n [\"100.64.0.0\", 10],\n [\"127.0.0.0\", 8],\n [\"169.254.0.0\", 16],\n [\"172.16.0.0\", 12],\n [\"192.0.0.0\", 24],\n [\"192.0.2.0\", 24],\n [\"192.88.99.0\", 24],\n [\"192.168.0.0\", 16],\n [\"198.18.0.0\", 15],\n [\"198.51.100.0\", 24],\n [\"203.0.113.0\", 24],\n [\"224.0.0.0\", 4],\n [\"240.0.0.0\", 4],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv4\");\n}\n\nfor (const [network, prefix] of [\n [\"::\", 96],\n [\"fc00::\", 7],\n [\"fe80::\", 10],\n [\"ff00::\", 8],\n [\"2001::\", 32],\n [\"2001:2::\", 48],\n [\"2001:10::\", 28],\n [\"2001:db8::\", 32],\n [\"2002::\", 16],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv6\");\n}\n\nconst publicIpv6Addresses = new BlockList();\npublicIpv6Addresses.addSubnet(\"2000::\", 3, \"ipv6\");\n\nconst blockedHostnames = new Set([\n \"localhost\",\n \"localhost.localdomain\",\n \"ip6-localhost\",\n \"ip6-loopback\",\n]);\n\nconst blockedHostnameSuffixes = [\n \".localhost\",\n \".local\",\n \".internal\",\n \".home.arpa\",\n \".test\",\n \".invalid\",\n \".example\",\n \".onion\",\n];\n\nconst redirectStatuses = new Set([301, 302, 303, 307, 308]);\n\nexport interface PublicFetchOptions {\n method?: \"GET\" | \"HEAD\";\n headers?: Record<string, string>;\n timeoutMs?: number;\n maxRedirects?: number;\n}\n\nexport interface PublicTextOptions extends PublicFetchOptions {\n maxBytes?: number;\n acceptedContentTypes?: readonly string[];\n}\n\nexport interface PublicTextResult {\n text: string;\n url: string;\n status: number;\n contentType: string | null;\n}\n\nexport interface PublicHttpResult {\n response: Response;\n url: URL;\n}\n\nexport class PublicUrlError extends Error {\n override readonly name = \"PublicUrlError\";\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n if (error.name === \"TimeoutError\" || error.name === \"AbortError\") {\n return \"Request timed out\";\n }\n return error.message || error.name;\n }\n\n return typeof error === \"string\" && error.trim() ? error : \"Unknown error\";\n}\n\nfunction assertPositiveInteger(value: number, label: string): void {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n}\n\nfunction normalizeHostname(hostname: string): string {\n const withoutBrackets = hostname.startsWith(\"[\") && hostname.endsWith(\"]\")\n ? hostname.slice(1, -1)\n : hostname;\n return withoutBrackets.replace(/\\.$/, \"\").toLowerCase();\n}\n\nfunction isPublicAddress(address: string, family: number): boolean {\n if (family === 4) {\n return !blockedAddresses.check(address, \"ipv4\");\n }\n\n if (family === 6) {\n return publicIpv6Addresses.check(address, \"ipv6\")\n && !blockedAddresses.check(address, \"ipv6\");\n }\n\n return false;\n}\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n let timeout: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<T>((_, reject) => {\n timeout = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) {\n clearTimeout(timeout);\n }\n }\n}\n\n/**\n * Parses an HTTP(S) URL and rejects hostnames that resolve to local, private,\n * documentation, multicast, or otherwise non-public address space.\n */\nexport async function assertPublicHttpUrl(input: string | URL): Promise<URL> {\n const rawUrl = input instanceof URL ? input.href : input;\n if (typeof rawUrl !== \"string\" || rawUrl.length === 0 || rawUrl.length > MAX_URL_LENGTH) {\n throw new PublicUrlError(`URL must contain between 1 and ${MAX_URL_LENGTH} characters`);\n }\n\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n throw new PublicUrlError(\"URL must be an absolute HTTP or HTTPS URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new PublicUrlError(\"Only HTTP and HTTPS URLs are allowed\");\n }\n if (parsed.username || parsed.password) {\n throw new PublicUrlError(\"URLs containing credentials are not allowed\");\n }\n if (parsed.port && parsed.port !== \"80\" && parsed.port !== \"443\") {\n throw new PublicUrlError(\"Only standard HTTP ports 80 and 443 are allowed\");\n }\n\n parsed.hash = \"\";\n const hostname = normalizeHostname(parsed.hostname);\n if (!hostname) {\n throw new PublicUrlError(\"URL hostname is required\");\n }\n if (\n blockedHostnames.has(hostname)\n || blockedHostnameSuffixes.some((suffix) => hostname.endsWith(suffix))\n ) {\n throw new PublicUrlError(`URL hostname \"${hostname}\" is not public`);\n }\n\n const literalFamily = isIP(hostname);\n const resolvedAddresses = literalFamily\n ? [{ address: hostname, family: literalFamily }]\n : await withTimeout(\n lookup(hostname, { all: true, verbatim: true }),\n DNS_TIMEOUT_MS,\n \"DNS lookup\",\n ).catch((error: unknown) => {\n throw new PublicUrlError(`Unable to resolve URL hostname: ${getErrorMessage(error)}`);\n });\n\n if (resolvedAddresses.length === 0) {\n throw new PublicUrlError(\"URL hostname did not resolve to an address\");\n }\n\n for (const record of resolvedAddresses) {\n if (!isPublicAddress(record.address, record.family)) {\n throw new PublicUrlError(`URL hostname resolves to non-public address ${record.address}`);\n }\n }\n\n return parsed;\n}\n\nexport async function cancelResponseBody(response: Response): Promise<void> {\n if (!response.body) {\n return;\n }\n\n try {\n await response.body.cancel();\n } catch {\n // The stream may already be closed or consumed; there is nothing left to release.\n }\n}\n\nexport async function readResponseText(response: Response, maxBytes: number): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const declaredLength = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {\n await cancelResponseBody(response);\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n\n if (!response.body) {\n return \"\";\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let totalBytes = 0;\n let text = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n text += decoder.decode(value, { stream: true });\n }\n text += decoder.decode();\n return text;\n } finally {\n reader.releaseLock();\n }\n}\n\n/** Fetches a public HTTP(S) URL while validating every redirect target. */\nexport async function fetchPublicHttp(\n input: string | URL,\n options: PublicFetchOptions = {},\n): Promise<PublicHttpResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n assertPositiveInteger(timeoutMs, \"timeoutMs\");\n if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0 || maxRedirects > 10) {\n throw new Error(\"maxRedirects must be an integer between 0 and 10\");\n }\n\n let currentUrl = await assertPublicHttpUrl(input);\n for (let redirectCount = 0; ; redirectCount += 1) {\n const response = await fetch(currentUrl, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n redirect: \"manual\",\n signal: AbortSignal.timeout(timeoutMs),\n });\n\n if (!redirectStatuses.has(response.status)) {\n return { response, url: currentUrl };\n }\n\n const location = response.headers.get(\"location\");\n if (!location) {\n return { response, url: currentUrl };\n }\n\n if (redirectCount >= maxRedirects) {\n return { response, url: currentUrl };\n }\n\n await cancelResponseBody(response);\n let redirectUrl: URL;\n try {\n redirectUrl = new URL(location, currentUrl);\n } catch {\n throw new PublicUrlError(\"Redirect target is not a valid URL\");\n }\n currentUrl = await assertPublicHttpUrl(redirectUrl);\n }\n}\n\n/** Fetches bounded text from a public URL and rejects non-success responses. */\nexport async function fetchPublicText(\n input: string | URL,\n options: PublicTextOptions = {},\n): Promise<PublicTextResult> {\n const maxBytes = options.maxBytes ?? 2_000_000;\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const { response, url } = await fetchPublicHttp(input, {\n method: \"GET\",\n headers: options.headers,\n timeoutMs: options.timeoutMs,\n maxRedirects: options.maxRedirects,\n });\n\n if (!response.ok) {\n const status = response.status;\n await cancelResponseBody(response);\n throw new Error(`URL returned HTTP status ${status}`);\n }\n\n const contentTypeHeader = response.headers.get(\"content-type\");\n const contentType = contentTypeHeader?.split(\";\", 1)[0]?.trim().toLowerCase() || null;\n if (\n options.acceptedContentTypes\n && !options.acceptedContentTypes.some((accepted) => accepted.trim().toLowerCase() === contentType)\n ) {\n await cancelResponseBody(response);\n throw new Error(`URL returned unsupported content type ${contentType ?? \"missing\"}`);\n }\n\n return {\n text: await readResponseText(response, maxBytes),\n url: url.href,\n status: response.status,\n contentType: contentTypeHeader,\n };\n}\n\n/** Reads a UTF-8 file only when it is a regular file within the requested cap. */\nexport async function readTextFile(filePath: string, maxBytes: number): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n const resolvedPath = path.resolve(filePath);\n const stats = await fs.stat(resolvedPath);\n if (!stats.isFile()) {\n throw new Error(`Path is not a regular file: ${resolvedPath}`);\n }\n if (stats.size > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n\n const contents = await fs.readFile(resolvedPath);\n if (contents.byteLength > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n return contents.toString(\"utf8\");\n}\n"]}
1
+ {"version":3,"file":"network.js","sourceRoot":"","sources":["../src/network.ts"],"names":[],"mappings":"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","sourcesContent":["import { lookup } from \"node:dns/promises\";\nimport * as fs from \"node:fs/promises\";\nimport { BlockList, isIP } from \"node:net\";\nimport * as path from \"node:path\";\n\nconst DEFAULT_TIMEOUT_MS = 10_000;\nconst DEFAULT_MAX_REDIRECTS = 5;\nconst DNS_TIMEOUT_MS = 5_000;\nconst MAX_URL_LENGTH = 8_192;\n\nconst blockedAddresses = new BlockList();\nfor (const [network, prefix] of [\n [\"0.0.0.0\", 8],\n [\"10.0.0.0\", 8],\n [\"100.64.0.0\", 10],\n [\"127.0.0.0\", 8],\n [\"169.254.0.0\", 16],\n [\"172.16.0.0\", 12],\n [\"192.0.0.0\", 24],\n [\"192.0.2.0\", 24],\n [\"192.88.99.0\", 24],\n [\"192.168.0.0\", 16],\n [\"198.18.0.0\", 15],\n [\"198.51.100.0\", 24],\n [\"203.0.113.0\", 24],\n [\"224.0.0.0\", 4],\n [\"240.0.0.0\", 4],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv4\");\n}\n\nfor (const [network, prefix] of [\n [\"::\", 96],\n [\"fc00::\", 7],\n [\"fe80::\", 10],\n [\"ff00::\", 8],\n [\"2001::\", 32],\n [\"2001:2::\", 48],\n [\"2001:10::\", 28],\n [\"2001:db8::\", 32],\n [\"2002::\", 16],\n] as const) {\n blockedAddresses.addSubnet(network, prefix, \"ipv6\");\n}\n\nconst publicIpv6Addresses = new BlockList();\npublicIpv6Addresses.addSubnet(\"2000::\", 3, \"ipv6\");\n\nconst blockedHostnames = new Set([\n \"localhost\",\n \"localhost.localdomain\",\n \"ip6-localhost\",\n \"ip6-loopback\",\n]);\n\nconst blockedHostnameSuffixes = [\n \".localhost\",\n \".local\",\n \".internal\",\n \".home.arpa\",\n \".test\",\n \".invalid\",\n \".example\",\n \".onion\",\n];\n\nconst redirectStatuses = new Set([301, 302, 303, 307, 308]);\n\nexport interface PublicFetchOptions {\n method?: \"GET\" | \"HEAD\";\n headers?: Record<string, string>;\n timeoutMs?: number;\n maxRedirects?: number;\n}\n\nexport interface PublicTextOptions extends PublicFetchOptions {\n maxBytes?: number;\n acceptedContentTypes?: readonly string[];\n}\n\nexport interface PublicTextResult {\n text: string;\n url: string;\n status: number;\n contentType: string | null;\n}\n\nexport interface PublicHttpResult {\n response: Response;\n url: URL;\n}\n\nexport class PublicUrlError extends Error {\n override readonly name = \"PublicUrlError\";\n}\n\nexport function getErrorMessage(error: unknown): string {\n if (error instanceof Error) {\n if (error.name === \"TimeoutError\" || error.name === \"AbortError\") {\n return \"Request timed out\";\n }\n return error.message || error.name;\n }\n\n return typeof error === \"string\" && error.trim() ? error : \"Unknown error\";\n}\n\nfunction assertPositiveInteger(value: number, label: string): void {\n if (!Number.isSafeInteger(value) || value <= 0) {\n throw new Error(`${label} must be a positive integer`);\n }\n}\n\nfunction normalizeHostname(hostname: string): string {\n const withoutBrackets = hostname.startsWith(\"[\") && hostname.endsWith(\"]\")\n ? hostname.slice(1, -1)\n : hostname;\n return withoutBrackets.replace(/\\.$/, \"\").toLowerCase();\n}\n\nfunction isPublicAddress(address: string, family: number): boolean {\n if (family === 4) {\n return !blockedAddresses.check(address, \"ipv4\");\n }\n\n if (family === 6) {\n return publicIpv6Addresses.check(address, \"ipv6\")\n && !blockedAddresses.check(address, \"ipv6\");\n }\n\n return false;\n}\n\nasync function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {\n let timeout: NodeJS.Timeout | undefined;\n try {\n return await Promise.race([\n promise,\n new Promise<T>((_, reject) => {\n timeout = setTimeout(() => reject(new Error(`${label} timed out`)), timeoutMs);\n }),\n ]);\n } finally {\n if (timeout) {\n clearTimeout(timeout);\n }\n }\n}\n\n/**\n * Parses an HTTP(S) URL and rejects hostnames that resolve to local, private,\n * documentation, multicast, or otherwise non-public address space.\n */\nexport async function assertPublicHttpUrl(input: string | URL): Promise<URL> {\n const rawUrl = input instanceof URL ? input.href : input;\n if (typeof rawUrl !== \"string\" || rawUrl.length === 0 || rawUrl.length > MAX_URL_LENGTH) {\n throw new PublicUrlError(`URL must contain between 1 and ${MAX_URL_LENGTH} characters`);\n }\n\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n throw new PublicUrlError(\"URL must be an absolute HTTP or HTTPS URL\");\n }\n\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new PublicUrlError(\"Only HTTP and HTTPS URLs are allowed\");\n }\n if (parsed.username || parsed.password) {\n throw new PublicUrlError(\"URLs containing credentials are not allowed\");\n }\n // URL normalizes matching defaults (HTTP :80 / HTTPS :443) to an empty port.\n // Any remaining port is custom or mismatched to the scheme and must be rejected.\n if (parsed.port) {\n throw new PublicUrlError(\"Only default HTTP and HTTPS ports are allowed\");\n }\n\n parsed.hash = \"\";\n const hostname = normalizeHostname(parsed.hostname);\n if (!hostname) {\n throw new PublicUrlError(\"URL hostname is required\");\n }\n if (\n blockedHostnames.has(hostname)\n || blockedHostnameSuffixes.some((suffix) => hostname.endsWith(suffix))\n ) {\n throw new PublicUrlError(`URL hostname \"${hostname}\" is not public`);\n }\n\n const literalFamily = isIP(hostname);\n const resolvedAddresses = literalFamily\n ? [{ address: hostname, family: literalFamily }]\n : await withTimeout(\n lookup(hostname, { all: true, verbatim: true }),\n DNS_TIMEOUT_MS,\n \"DNS lookup\",\n ).catch((error: unknown) => {\n throw new PublicUrlError(`Unable to resolve URL hostname: ${getErrorMessage(error)}`);\n });\n\n if (resolvedAddresses.length === 0) {\n throw new PublicUrlError(\"URL hostname did not resolve to an address\");\n }\n\n for (const record of resolvedAddresses) {\n if (!isPublicAddress(record.address, record.family)) {\n throw new PublicUrlError(`URL hostname resolves to non-public address ${record.address}`);\n }\n }\n\n return parsed;\n}\n\nexport async function cancelResponseBody(response: Response): Promise<void> {\n if (!response.body) {\n return;\n }\n\n try {\n await response.body.cancel();\n } catch {\n // The stream may already be closed or consumed; there is nothing left to release.\n }\n}\n\nexport async function readResponseText(response: Response, maxBytes: number): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const declaredLength = Number(response.headers.get(\"content-length\"));\n if (Number.isFinite(declaredLength) && declaredLength > maxBytes) {\n await cancelResponseBody(response);\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n\n if (!response.body) {\n return \"\";\n }\n\n const reader = response.body.getReader();\n const decoder = new TextDecoder();\n let totalBytes = 0;\n let text = \"\";\n\n try {\n while (true) {\n const { done, value } = await reader.read();\n if (done) {\n break;\n }\n\n totalBytes += value.byteLength;\n if (totalBytes > maxBytes) {\n await reader.cancel();\n throw new Error(`Response exceeds the ${maxBytes}-byte limit`);\n }\n text += decoder.decode(value, { stream: true });\n }\n text += decoder.decode();\n return text;\n } finally {\n reader.releaseLock();\n }\n}\n\n/** Fetches a public HTTP(S) URL while validating every redirect target. */\nexport async function fetchPublicHttp(\n input: string | URL,\n options: PublicFetchOptions = {},\n): Promise<PublicHttpResult> {\n const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const maxRedirects = options.maxRedirects ?? DEFAULT_MAX_REDIRECTS;\n assertPositiveInteger(timeoutMs, \"timeoutMs\");\n if (!Number.isSafeInteger(maxRedirects) || maxRedirects < 0 || maxRedirects > 10) {\n throw new Error(\"maxRedirects must be an integer between 0 and 10\");\n }\n\n let currentUrl = await assertPublicHttpUrl(input);\n for (let redirectCount = 0; ; redirectCount += 1) {\n const response = await fetch(currentUrl, {\n method: options.method ?? \"GET\",\n headers: options.headers,\n redirect: \"manual\",\n signal: AbortSignal.timeout(timeoutMs),\n });\n\n if (!redirectStatuses.has(response.status)) {\n return { response, url: currentUrl };\n }\n\n const location = response.headers.get(\"location\");\n if (!location) {\n return { response, url: currentUrl };\n }\n\n if (redirectCount >= maxRedirects) {\n return { response, url: currentUrl };\n }\n\n await cancelResponseBody(response);\n let redirectUrl: URL;\n try {\n redirectUrl = new URL(location, currentUrl);\n } catch {\n throw new PublicUrlError(\"Redirect target is not a valid URL\");\n }\n currentUrl = await assertPublicHttpUrl(redirectUrl);\n }\n}\n\n/** Fetches bounded text from a public URL and rejects non-success responses. */\nexport async function fetchPublicText(\n input: string | URL,\n options: PublicTextOptions = {},\n): Promise<PublicTextResult> {\n const maxBytes = options.maxBytes ?? 2_000_000;\n assertPositiveInteger(maxBytes, \"maxBytes\");\n\n const { response, url } = await fetchPublicHttp(input, {\n method: \"GET\",\n headers: options.headers,\n timeoutMs: options.timeoutMs,\n maxRedirects: options.maxRedirects,\n });\n\n if (!response.ok) {\n const status = response.status;\n await cancelResponseBody(response);\n throw new Error(`URL returned HTTP status ${status}`);\n }\n\n const contentTypeHeader = response.headers.get(\"content-type\");\n const contentType = contentTypeHeader?.split(\";\", 1)[0]?.trim().toLowerCase() || null;\n if (\n options.acceptedContentTypes\n && !options.acceptedContentTypes.some((accepted) => accepted.trim().toLowerCase() === contentType)\n ) {\n await cancelResponseBody(response);\n throw new Error(`URL returned unsupported content type ${contentType ?? \"missing\"}`);\n }\n\n return {\n text: await readResponseText(response, maxBytes),\n url: url.href,\n status: response.status,\n contentType: contentTypeHeader,\n };\n}\n\n/** Reads a UTF-8 file only when it is a regular file within the requested cap. */\nexport async function readTextFile(filePath: string, maxBytes: number): Promise<string> {\n assertPositiveInteger(maxBytes, \"maxBytes\");\n const resolvedPath = path.resolve(filePath);\n const stats = await fs.stat(resolvedPath);\n if (!stats.isFile()) {\n throw new Error(`Path is not a regular file: ${resolvedPath}`);\n }\n if (stats.size > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n\n const contents = await fs.readFile(resolvedPath);\n if (contents.byteLength > maxBytes) {\n throw new Error(`File exceeds the ${maxBytes}-byte limit: ${resolvedPath}`);\n }\n return contents.toString(\"utf8\");\n}\n"]}
package/dist/report.js CHANGED
@@ -40,7 +40,9 @@ export function createValidationReport(input) {
40
40
  const cssScore = checkFailed("css") || !input.cssAudited || cssCompatibilityLimitations > 0
41
41
  ? null
42
42
  : clampScore(100 - cssErrors * 20);
43
- const seoScore = checkFailed("seo") ? null : clampScore(100 - seoErrors * 15 - seoWarnings * 4 - schemaErrors * 15);
43
+ const seoScore = checkFailed("seo") || checkFailed("schema")
44
+ ? null
45
+ : clampScore(100 - seoErrors * 15 - seoWarnings * 4 - schemaErrors * 15);
44
46
  const linkScore = checkFailed("links") || input.links.length === 0
45
47
  ? null
46
48
  : clampScore(100 - brokenLinks * 25);
@@ -1 +1 @@
1
- {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from \"node:path\";\nimport type { CSSMessage, W3CMessage } from \"./w3c-validator.js\";\nimport type { LinkStatus, SEOIssue } from \"./seo-auditor.js\";\n\nexport const validationReportChecks = [\"input\", \"html\", \"css\", \"seo\", \"schema\", \"links\"] as const;\nexport type ValidationReportCheck = (typeof validationReportChecks)[number];\n\nexport interface ValidationReportSummary {\n overallScore: number | null;\n htmlScore: number | null;\n cssScore: number | null;\n seoScore: number | null;\n linkScore: number | null;\n htmlErrors: number;\n htmlWarnings: number;\n cssErrors: number;\n cssCompatibilityLimitations: number;\n seoErrors: number;\n seoWarnings: number;\n schemaErrors: number;\n linksChecked: number;\n brokenLinks: number;\n}\n\nexport interface ValidationReport {\n report: string;\n summary: ValidationReportSummary;\n htmlMessages: W3CMessage[];\n cssMessages: CSSMessage[];\n seoIssues: SEOIssue[];\n schemaIssues: SEOIssue[];\n links: LinkStatus[];\n failedChecks: ValidationReportCheck[];\n errors?: string[];\n}\n\nexport interface ValidationReportInput {\n htmlFilePath: string;\n cssAudited: boolean;\n htmlMessages: W3CMessage[];\n cssMessages: CSSMessage[];\n seoIssues: SEOIssue[];\n schemaIssues: SEOIssue[];\n links: LinkStatus[];\n failedChecks?: ValidationReportCheck[];\n errors?: string[];\n}\n\nfunction clampScore(score: number): number {\n return Math.max(0, Math.min(100, score));\n}\n\nfunction scoreIndicator(score: number | null): string {\n if (score === null) return \"Unavailable\";\n if (score >= 90) return \"🟢\";\n if (score >= 50) return \"🟠\";\n return \"🔴\";\n}\n\nfunction markdownCell(value: unknown): string {\n return String(value ?? \"\")\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\\|/g, \"\\\\|\")\n .replace(/`/g, \"\\\\`\")\n .replace(/[\\r\\n]+/g, \" \")\n .trim();\n}\n\nfunction isRedirect(link: LinkStatus): boolean {\n return typeof link.status === \"number\" && link.status >= 300 && link.status < 400;\n}\n\n/** Builds the human-readable report and its machine-readable equivalent. */\nexport function createValidationReport(input: ValidationReportInput): ValidationReport {\n const failedChecks = input.failedChecks ?? [];\n const checkFailed = (check: ValidationReportCheck): boolean => failedChecks.includes(check);\n const htmlErrors = input.htmlMessages.filter((message) => message.type === \"error\").length;\n const htmlWarnings = input.htmlMessages.length - htmlErrors;\n const cssErrors = input.cssMessages.length;\n const cssCompatibilityLimitations = input.cssMessages.filter(\n (message) => message.compatibility === \"known-validator-limitation\",\n ).length;\n const seoErrors = input.seoIssues.filter((issue) => issue.severity === \"error\").length;\n const seoWarnings = input.seoIssues.filter((issue) => issue.severity === \"warning\").length;\n const schemaErrors = input.schemaIssues.filter((issue) => issue.severity === \"error\").length;\n const brokenLinks = input.links.filter((link) => !link.ok).length;\n const redirectLinks = input.links.filter(isRedirect).length;\n\n const htmlScore = checkFailed(\"html\") ? null : clampScore(100 - htmlErrors * 15 - htmlWarnings * 2);\n const cssScore = checkFailed(\"css\") || !input.cssAudited || cssCompatibilityLimitations > 0\n ? null\n : clampScore(100 - cssErrors * 20);\n const seoScore = checkFailed(\"seo\") ? null : clampScore(100 - seoErrors * 15 - seoWarnings * 4 - schemaErrors * 15);\n const linkScore = checkFailed(\"links\") || input.links.length === 0\n ? null\n : clampScore(100 - brokenLinks * 25);\n const auditedScores = [htmlScore, seoScore, cssScore, linkScore].filter(\n (score): score is number => score !== null,\n );\n const overallScore = failedChecks.length > 0 || cssCompatibilityLimitations > 0 || auditedScores.length === 0\n ? null\n : Math.round(auditedScores.reduce((total, score) => total + score, 0) / auditedScores.length);\n\n const summary: ValidationReportSummary = {\n overallScore,\n htmlScore,\n cssScore,\n seoScore,\n linkScore,\n htmlErrors,\n htmlWarnings,\n cssErrors,\n cssCompatibilityLimitations,\n seoErrors,\n seoWarnings,\n schemaErrors,\n linksChecked: input.links.length,\n brokenLinks,\n };\n\n const report: string[] = [\n `# Web Validation & SEO Audit Report — ${overallScore === null ? \"Partial\" : `${scoreIndicator(overallScore)} **${overallScore}**/100`}`,\n `*Generated for: \\`${markdownCell(path.basename(input.htmlFilePath))}\\`*`,\n \"\",\n \"## Page health scores\",\n \"\",\n \"| Audit | Status | Score |\",\n \"| :--- | :---: | :---: |\",\n `| W3C HTML validation | ${htmlScore === null ? \"Unavailable\" : scoreIndicator(htmlScore)} | ${htmlScore === null ? \"N/A\" : `**${htmlScore}** / 100`} |`,\n `| CSS validation | ${cssScore === null ? (checkFailed(\"css\") ? \"Unavailable\" : cssCompatibilityLimitations > 0 ? \"Compatibility-limited\" : \"Not audited\") : scoreIndicator(cssScore)} | ${cssScore === null ? \"N/A\" : `**${cssScore}** / 100`} |`,\n `| SEO and accessibility | ${seoScore === null ? \"Unavailable\" : scoreIndicator(seoScore)} | ${seoScore === null ? \"N/A\" : `**${seoScore}** / 100`} |`,\n `| Link integrity | ${linkScore === null ? (checkFailed(\"links\") ? \"Unavailable\" : \"No links checked\") : scoreIndicator(linkScore)} | ${linkScore === null ? \"N/A\" : `**${linkScore}** / 100`} |`,\n \"\",\n \"## Summary\",\n \"\",\n `- HTML: ${htmlErrors} error(s), ${htmlWarnings} other diagnostic(s)` ,\n `- CSS: ${input.cssAudited ? `${cssErrors} error(s)${cssCompatibilityLimitations > 0 ? `, ${cssCompatibilityLimitations} known validator limitation(s)` : \"\"}` : \"not audited\"}`,\n `- SEO and accessibility: ${seoErrors} error(s), ${seoWarnings} warning(s)`,\n `- JSON-LD syntax: ${schemaErrors} error(s)`,\n `- Links: ${brokenLinks} broken or unreachable, ${redirectLinks} redirect${redirectLinks === 1 ? \"\" : \"s\"} to review of ${input.links.length} checked`,\n \"\",\n `## HTML diagnostics (${input.htmlMessages.length})`,\n ];\n\n if (failedChecks.length > 0) {\n report.splice(2, 0, \"\", \"## Partial validation report\", \"The following checks were unavailable: \" + failedChecks.join(\", \") + \". Remaining checks completed.\");\n }\n\n if (input.htmlMessages.length === 0) {\n report.push(\"No HTML validation diagnostics were returned.\");\n } else {\n report.push(\"\", \"| Line | Column | Severity | Message | Extract |\", \"| :---: | :---: | :--- | :--- | :--- |\");\n for (const message of input.htmlMessages) {\n report.push(\n `| ${message.lastLine ?? \"N/A\"} | ${message.lastColumn ?? \"N/A\"} | ${markdownCell(message.type)} | ${markdownCell(message.message)} | ${markdownCell(message.extract ?? \"N/A\")} |`,\n );\n }\n }\n\n if (input.cssAudited) {\n report.push(\"\", `## CSS diagnostics (${input.cssMessages.length})`);\n if (input.cssMessages.length === 0) {\n report.push(\"No CSS validation errors were returned.\");\n } else {\n report.push(\"\", \"| Line | Context | Message |\", \"| :---: | :--- | :--- |\");\n for (const message of input.cssMessages) {\n report.push(\n `| ${message.line} | ${markdownCell(message.context ?? \"N/A\")} | ${markdownCell(message.message)} |`,\n );\n }\n if (cssCompatibilityLimitations > 0) {\n report.push(\n \"\",\n `${cssCompatibilityLimitations} known validator limitation(s) match Jigsaw's current parser gap for the standards-defined \\`@container\\` rule. The original Jigsaw diagnostic is preserved, but CSS and overall scores are withheld because this upstream limitation can report valid modern CSS as invalid.`,\n );\n }\n }\n }\n\n const combinedIssues = [...input.seoIssues, ...input.schemaIssues];\n report.push(\"\", `## SEO, accessibility, and JSON-LD findings (${combinedIssues.length})`);\n if (combinedIssues.length === 0) {\n report.push(\"No SEO, accessibility, or JSON-LD syntax findings were returned.\");\n } else {\n report.push(\"\", \"| Category | Severity | Message | Element |\", \"| :--- | :--- | :--- | :--- |\");\n for (const issue of combinedIssues) {\n report.push(\n `| ${markdownCell(issue.category)} | ${markdownCell(issue.severity)} | ${markdownCell(issue.message)} | ${markdownCell(issue.element ?? \"N/A\")} |`,\n );\n }\n }\n\n report.push(\"\", `## Link health (${input.links.length} checked)`);\n if (input.links.length === 0) {\n report.push(\"No public HTTP(S) links were checked.\");\n } else {\n report.push(\"\", \"| URL | Status | Reachable | Details |\", \"| :--- | :---: | :---: | :--- |\");\n for (const link of input.links) {\n report.push(\n `| ${markdownCell(link.url)} | ${markdownCell(link.status)} | ${link.ok ? \"Yes\" : \"No\"} | ${markdownCell(link.message ?? \"Accessible\")} |`,\n );\n }\n }\n\n return {\n report: report.join(\"\\n\"),\n summary,\n htmlMessages: input.htmlMessages,\n cssMessages: input.cssMessages,\n seoIssues: input.seoIssues,\n schemaIssues: input.schemaIssues,\n links: input.links,\n failedChecks,\n ...(input.errors && input.errors.length > 0 ? { errors: input.errors } : {}),\n };\n}\n"]}
1
+ {"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from \"node:path\";\nimport type { CSSMessage, W3CMessage } from \"./w3c-validator.js\";\nimport type { LinkStatus, SEOIssue } from \"./seo-auditor.js\";\n\nexport const validationReportChecks = [\"input\", \"html\", \"css\", \"seo\", \"schema\", \"links\"] as const;\nexport type ValidationReportCheck = (typeof validationReportChecks)[number];\n\nexport interface ValidationReportSummary {\n overallScore: number | null;\n htmlScore: number | null;\n cssScore: number | null;\n seoScore: number | null;\n linkScore: number | null;\n htmlErrors: number;\n htmlWarnings: number;\n cssErrors: number;\n cssCompatibilityLimitations: number;\n seoErrors: number;\n seoWarnings: number;\n schemaErrors: number;\n linksChecked: number;\n brokenLinks: number;\n}\n\nexport interface ValidationReport {\n report: string;\n summary: ValidationReportSummary;\n htmlMessages: W3CMessage[];\n cssMessages: CSSMessage[];\n seoIssues: SEOIssue[];\n schemaIssues: SEOIssue[];\n links: LinkStatus[];\n failedChecks: ValidationReportCheck[];\n errors?: string[];\n}\n\nexport interface ValidationReportInput {\n htmlFilePath: string;\n cssAudited: boolean;\n htmlMessages: W3CMessage[];\n cssMessages: CSSMessage[];\n seoIssues: SEOIssue[];\n schemaIssues: SEOIssue[];\n links: LinkStatus[];\n failedChecks?: ValidationReportCheck[];\n errors?: string[];\n}\n\nfunction clampScore(score: number): number {\n return Math.max(0, Math.min(100, score));\n}\n\nfunction scoreIndicator(score: number | null): string {\n if (score === null) return \"Unavailable\";\n if (score >= 90) return \"🟢\";\n if (score >= 50) return \"🟠\";\n return \"🔴\";\n}\n\nfunction markdownCell(value: unknown): string {\n return String(value ?? \"\")\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/\\|/g, \"\\\\|\")\n .replace(/`/g, \"\\\\`\")\n .replace(/[\\r\\n]+/g, \" \")\n .trim();\n}\n\nfunction isRedirect(link: LinkStatus): boolean {\n return typeof link.status === \"number\" && link.status >= 300 && link.status < 400;\n}\n\n/** Builds the human-readable report and its machine-readable equivalent. */\nexport function createValidationReport(input: ValidationReportInput): ValidationReport {\n const failedChecks = input.failedChecks ?? [];\n const checkFailed = (check: ValidationReportCheck): boolean => failedChecks.includes(check);\n const htmlErrors = input.htmlMessages.filter((message) => message.type === \"error\").length;\n const htmlWarnings = input.htmlMessages.length - htmlErrors;\n const cssErrors = input.cssMessages.length;\n const cssCompatibilityLimitations = input.cssMessages.filter(\n (message) => message.compatibility === \"known-validator-limitation\",\n ).length;\n const seoErrors = input.seoIssues.filter((issue) => issue.severity === \"error\").length;\n const seoWarnings = input.seoIssues.filter((issue) => issue.severity === \"warning\").length;\n const schemaErrors = input.schemaIssues.filter((issue) => issue.severity === \"error\").length;\n const brokenLinks = input.links.filter((link) => !link.ok).length;\n const redirectLinks = input.links.filter(isRedirect).length;\n\n const htmlScore = checkFailed(\"html\") ? null : clampScore(100 - htmlErrors * 15 - htmlWarnings * 2);\n const cssScore = checkFailed(\"css\") || !input.cssAudited || cssCompatibilityLimitations > 0\n ? null\n : clampScore(100 - cssErrors * 20);\n const seoScore = checkFailed(\"seo\") || checkFailed(\"schema\")\n ? null\n : clampScore(100 - seoErrors * 15 - seoWarnings * 4 - schemaErrors * 15);\n const linkScore = checkFailed(\"links\") || input.links.length === 0\n ? null\n : clampScore(100 - brokenLinks * 25);\n const auditedScores = [htmlScore, seoScore, cssScore, linkScore].filter(\n (score): score is number => score !== null,\n );\n const overallScore = failedChecks.length > 0 || cssCompatibilityLimitations > 0 || auditedScores.length === 0\n ? null\n : Math.round(auditedScores.reduce((total, score) => total + score, 0) / auditedScores.length);\n\n const summary: ValidationReportSummary = {\n overallScore,\n htmlScore,\n cssScore,\n seoScore,\n linkScore,\n htmlErrors,\n htmlWarnings,\n cssErrors,\n cssCompatibilityLimitations,\n seoErrors,\n seoWarnings,\n schemaErrors,\n linksChecked: input.links.length,\n brokenLinks,\n };\n\n const report: string[] = [\n `# Web Validation & SEO Audit Report — ${overallScore === null ? \"Partial\" : `${scoreIndicator(overallScore)} **${overallScore}**/100`}`,\n `*Generated for: \\`${markdownCell(path.basename(input.htmlFilePath))}\\`*`,\n \"\",\n \"## Page health scores\",\n \"\",\n \"| Audit | Status | Score |\",\n \"| :--- | :---: | :---: |\",\n `| W3C HTML validation | ${htmlScore === null ? \"Unavailable\" : scoreIndicator(htmlScore)} | ${htmlScore === null ? \"N/A\" : `**${htmlScore}** / 100`} |`,\n `| CSS validation | ${cssScore === null ? (checkFailed(\"css\") ? \"Unavailable\" : cssCompatibilityLimitations > 0 ? \"Compatibility-limited\" : \"Not audited\") : scoreIndicator(cssScore)} | ${cssScore === null ? \"N/A\" : `**${cssScore}** / 100`} |`,\n `| SEO and accessibility | ${seoScore === null ? \"Unavailable\" : scoreIndicator(seoScore)} | ${seoScore === null ? \"N/A\" : `**${seoScore}** / 100`} |`,\n `| Link integrity | ${linkScore === null ? (checkFailed(\"links\") ? \"Unavailable\" : \"No links checked\") : scoreIndicator(linkScore)} | ${linkScore === null ? \"N/A\" : `**${linkScore}** / 100`} |`,\n \"\",\n \"## Summary\",\n \"\",\n `- HTML: ${htmlErrors} error(s), ${htmlWarnings} other diagnostic(s)` ,\n `- CSS: ${input.cssAudited ? `${cssErrors} error(s)${cssCompatibilityLimitations > 0 ? `, ${cssCompatibilityLimitations} known validator limitation(s)` : \"\"}` : \"not audited\"}`,\n `- SEO and accessibility: ${seoErrors} error(s), ${seoWarnings} warning(s)`,\n `- JSON-LD syntax: ${schemaErrors} error(s)`,\n `- Links: ${brokenLinks} broken or unreachable, ${redirectLinks} redirect${redirectLinks === 1 ? \"\" : \"s\"} to review of ${input.links.length} checked`,\n \"\",\n `## HTML diagnostics (${input.htmlMessages.length})`,\n ];\n\n if (failedChecks.length > 0) {\n report.splice(2, 0, \"\", \"## Partial validation report\", \"The following checks were unavailable: \" + failedChecks.join(\", \") + \". Remaining checks completed.\");\n }\n\n if (input.htmlMessages.length === 0) {\n report.push(\"No HTML validation diagnostics were returned.\");\n } else {\n report.push(\"\", \"| Line | Column | Severity | Message | Extract |\", \"| :---: | :---: | :--- | :--- | :--- |\");\n for (const message of input.htmlMessages) {\n report.push(\n `| ${message.lastLine ?? \"N/A\"} | ${message.lastColumn ?? \"N/A\"} | ${markdownCell(message.type)} | ${markdownCell(message.message)} | ${markdownCell(message.extract ?? \"N/A\")} |`,\n );\n }\n }\n\n if (input.cssAudited) {\n report.push(\"\", `## CSS diagnostics (${input.cssMessages.length})`);\n if (input.cssMessages.length === 0) {\n report.push(\"No CSS validation errors were returned.\");\n } else {\n report.push(\"\", \"| Line | Context | Message |\", \"| :---: | :--- | :--- |\");\n for (const message of input.cssMessages) {\n report.push(\n `| ${message.line} | ${markdownCell(message.context ?? \"N/A\")} | ${markdownCell(message.message)} |`,\n );\n }\n if (cssCompatibilityLimitations > 0) {\n report.push(\n \"\",\n `${cssCompatibilityLimitations} known validator limitation(s) match Jigsaw's current parser gap for the standards-defined \\`@container\\` rule. The original Jigsaw diagnostic is preserved, but CSS and overall scores are withheld because this upstream limitation can report valid modern CSS as invalid.`,\n );\n }\n }\n }\n\n const combinedIssues = [...input.seoIssues, ...input.schemaIssues];\n report.push(\"\", `## SEO, accessibility, and JSON-LD findings (${combinedIssues.length})`);\n if (combinedIssues.length === 0) {\n report.push(\"No SEO, accessibility, or JSON-LD syntax findings were returned.\");\n } else {\n report.push(\"\", \"| Category | Severity | Message | Element |\", \"| :--- | :--- | :--- | :--- |\");\n for (const issue of combinedIssues) {\n report.push(\n `| ${markdownCell(issue.category)} | ${markdownCell(issue.severity)} | ${markdownCell(issue.message)} | ${markdownCell(issue.element ?? \"N/A\")} |`,\n );\n }\n }\n\n report.push(\"\", `## Link health (${input.links.length} checked)`);\n if (input.links.length === 0) {\n report.push(\"No public HTTP(S) links were checked.\");\n } else {\n report.push(\"\", \"| URL | Status | Reachable | Details |\", \"| :--- | :---: | :---: | :--- |\");\n for (const link of input.links) {\n report.push(\n `| ${markdownCell(link.url)} | ${markdownCell(link.status)} | ${link.ok ? \"Yes\" : \"No\"} | ${markdownCell(link.message ?? \"Accessible\")} |`,\n );\n }\n }\n\n return {\n report: report.join(\"\\n\"),\n summary,\n htmlMessages: input.htmlMessages,\n cssMessages: input.cssMessages,\n seoIssues: input.seoIssues,\n schemaIssues: input.schemaIssues,\n links: input.links,\n failedChecks,\n ...(input.errors && input.errors.length > 0 ? { errors: input.errors } : {}),\n };\n}\n"]}
@@ -79,7 +79,7 @@ export function auditSeoMetadataDetailed(htmlContent) {
79
79
  }
80
80
  }
81
81
  // --- Meta Description Audits ---
82
- const metaDescription = $('meta[name="description"]');
82
+ const metaDescription = $('meta[name="description" i]');
83
83
  if (metaDescription.length === 0) {
84
84
  add({
85
85
  code: "seo.meta_description.missing_or_empty",
@@ -1 +1 @@
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 code: string;\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 code: \"seo.title.missing_or_empty\",\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 code: \"seo.title.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"The <title> tag is empty.\",\n });\n } else if (titleText.length < 30) {\n add({\n code: \"seo.title.length\",\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 code: \"seo.title.length\",\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 code: \"seo.meta_description.missing_or_empty\",\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 code: \"seo.meta_description.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Meta description content attribute is empty.\",\n });\n } else if (descText.length < 120) {\n add({\n code: \"seo.meta_description.length\",\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 code: \"seo.meta_description.length\",\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]\").filter((_, element) => {\n const rel = $(element).attr(\"rel\") ?? \"\";\n const hasCanonicalToken = rel\n .trim()\n .split(/[\\t\\n\\f\\r ]+/)\n .some((token) => token.toLowerCase() === \"canonical\");\n return hasCanonicalToken && Boolean($(element).attr(\"href\")?.trim());\n });\n if (canonical.length === 0) {\n add({\n code: \"seo.canonical.missing\",\n severity: \"warning\",\n category: \"SEO\",\n message: \"Missing or empty canonical link target. Add a non-empty href to <link rel=\\\"canonical\\\"> when this page needs an explicit canonical signal.\",\n });\n }\n\n // --- Viewport Meta Tag (Mobile Responsiveness) ---\n const viewport = $('meta[name=\"viewport\"]');\n if (viewport.length === 0) {\n add({\n code: \"seo.viewport.missing\",\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 code: \"seo.h1.missing\",\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 code: \"seo.h1.multiple\",\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 code: \"accessibility.image_alt.missing\",\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 code: \"accessibility.image_alt.empty\",\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 code: \"seo.open_graph.missing\",\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 code: \"schema.jsonld.empty\",\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 code: \"schema.jsonld.invalid_json\",\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"]}
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 code: string;\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 code: \"seo.title.missing_or_empty\",\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 code: \"seo.title.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"The <title> tag is empty.\",\n });\n } else if (titleText.length < 30) {\n add({\n code: \"seo.title.length\",\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 code: \"seo.title.length\",\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\" i]');\n if (metaDescription.length === 0) {\n add({\n code: \"seo.meta_description.missing_or_empty\",\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 code: \"seo.meta_description.missing_or_empty\",\n severity: \"error\",\n category: \"SEO\",\n message: \"Meta description content attribute is empty.\",\n });\n } else if (descText.length < 120) {\n add({\n code: \"seo.meta_description.length\",\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 code: \"seo.meta_description.length\",\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]\").filter((_, element) => {\n const rel = $(element).attr(\"rel\") ?? \"\";\n const hasCanonicalToken = rel\n .trim()\n .split(/[\\t\\n\\f\\r ]+/)\n .some((token) => token.toLowerCase() === \"canonical\");\n return hasCanonicalToken && Boolean($(element).attr(\"href\")?.trim());\n });\n if (canonical.length === 0) {\n add({\n code: \"seo.canonical.missing\",\n severity: \"warning\",\n category: \"SEO\",\n message: \"Missing or empty canonical link target. Add a non-empty href to <link rel=\\\"canonical\\\"> when this page needs an explicit canonical signal.\",\n });\n }\n\n // --- Viewport Meta Tag (Mobile Responsiveness) ---\n const viewport = $('meta[name=\"viewport\"]');\n if (viewport.length === 0) {\n add({\n code: \"seo.viewport.missing\",\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 code: \"seo.h1.missing\",\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 code: \"seo.h1.multiple\",\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 code: \"accessibility.image_alt.missing\",\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 code: \"accessibility.image_alt.empty\",\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 code: \"seo.open_graph.missing\",\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 code: \"schema.jsonld.empty\",\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 code: \"schema.jsonld.invalid_json\",\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"]}
@@ -20,11 +20,11 @@ function assertContentSize(content, maxBytes, label) {
20
20
  }
21
21
  function normalizeW3CMessage(message) {
22
22
  if (typeof message !== "object" || message === null) {
23
- return null;
23
+ throw new Error("W3C HTML validator returned a malformed message");
24
24
  }
25
25
  const candidate = message;
26
26
  if (typeof candidate.type !== "string" || typeof candidate.message !== "string") {
27
- return null;
27
+ throw new Error("W3C HTML validator returned a malformed message");
28
28
  }
29
29
  const normalized = {
30
30
  type: candidate.type,
@@ -71,7 +71,6 @@ export async function validateHtmlContent(htmlContent) {
71
71
  }
72
72
  return data.messages
73
73
  .map(normalizeW3CMessage)
74
- .filter((message) => message !== null)
75
74
  .slice(0, MAX_VALIDATION_MESSAGES);
76
75
  }
77
76
  catch (error) {
@@ -1 +1 @@
1
- {"version":3,"file":"w3c-validator.js","sourceRoot":"","sources":["../src/w3c-validator.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n cancelResponseBody,\n getErrorMessage,\n readResponseText,\n} from \"./network.js\";\nimport { PACKAGE_VERSION } from \"./version.js\";\n\nexport interface W3CMessage {\n type: string;\n lastLine?: number;\n lastColumn?: number;\n firstLine?: number;\n firstColumn?: number;\n message: string;\n extract?: string;\n}\n\nexport interface CSSMessage {\n line: number;\n type: string;\n message: string;\n context?: string;\n compatibility?: \"known-validator-limitation\";\n}\n\nconst VALIDATOR_TIMEOUT_MS = 20_000;\nconst MAX_HTML_BYTES = 2_000_000;\nexport const MAX_CSS_VALIDATION_BYTES = 128_000;\nconst MAX_VALIDATOR_RESPONSE_BYTES = 5_000_000;\nconst USER_AGENT = `mcp-web-validator/${PACKAGE_VERSION} (+https://digestseo.com/validator-mcp/)`;\nexport const MAX_VALIDATION_MESSAGES = 200;\n\nfunction isKnownCssValidatorLimitation(type: string | undefined, message: string): boolean {\n if (type?.trim().toLowerCase() !== \"at-rule\") return false;\n const normalized = message.trim().toLowerCase().replace(/[“”\"'‘’]/g, \"\");\n return normalized === \"unrecognized at-rule @container\";\n}\n\nfunction assertContentSize(content: string, maxBytes: number, label: string): void {\n const size = Buffer.byteLength(content, \"utf8\");\n if (size > maxBytes) {\n throw new Error(`${label} exceeds the ${maxBytes}-byte validation limit`);\n }\n}\n\nfunction normalizeW3CMessage(message: unknown): W3CMessage | null {\n if (typeof message !== \"object\" || message === null) {\n return null;\n }\n\n const candidate = message as Record<string, unknown>;\n if (typeof candidate.type !== \"string\" || typeof candidate.message !== \"string\") {\n return null;\n }\n\n const normalized: W3CMessage = {\n type: candidate.type,\n message: candidate.message,\n };\n\n for (const field of [\"lastLine\", \"lastColumn\", \"firstLine\", \"firstColumn\"] as const) {\n if (typeof candidate[field] === \"number\" && Number.isInteger(candidate[field])) {\n normalized[field] = candidate[field];\n }\n }\n\n if (typeof candidate.extract === \"string\") {\n normalized.extract = candidate.extract;\n }\n\n return normalized;\n}\n\n/**\n * Validates HTML using the W3C Nu HTML Checker API\n */\nexport async function validateHtmlContent(htmlContent: string): Promise<W3CMessage[]> {\n const url = \"https://validator.w3.org/nu/?out=json\";\n\n try {\n assertContentSize(htmlContent, MAX_HTML_BYTES, \"HTML content\");\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"User-Agent\": USER_AGENT,\n },\n body: htmlContent,\n redirect: \"error\",\n signal: AbortSignal.timeout(VALIDATOR_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n await cancelResponseBody(response);\n throw new Error(`W3C HTML validator returned HTTP status ${response.status}`);\n }\n\n const text = await readResponseText(response, MAX_VALIDATOR_RESPONSE_BYTES);\n const data = JSON.parse(text) as { messages?: unknown };\n if (data.messages === undefined) {\n throw new Error(\"W3C HTML validator returned an invalid response shape\");\n }\n if (!Array.isArray(data.messages)) {\n throw new Error(\"W3C HTML validator returned an invalid response shape\");\n }\n return data.messages\n .map(normalizeW3CMessage)\n .filter((message): message is W3CMessage => message !== null)\n .slice(0, MAX_VALIDATION_MESSAGES);\n } catch (error: unknown) {\n throw new Error(`HTML validation failed: ${getErrorMessage(error)}`);\n }\n}\n\n/**\n * Validates CSS using the W3C Jigsaw CSS Validator API\n */\nexport async function validateCssContent(cssContent: string): Promise<CSSMessage[]> {\n const url = \"https://jigsaw.w3.org/css-validator/validator\";\n\n try {\n assertContentSize(cssContent, MAX_CSS_VALIDATION_BYTES, \"CSS content\");\n const form = new FormData();\n form.set(\"text\", cssContent);\n form.set(\"output\", \"json\");\n form.set(\"warning\", \"0\");\n form.set(\"profile\", \"css3svg\");\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"User-Agent\": USER_AGENT,\n },\n body: form,\n redirect: \"error\",\n signal: AbortSignal.timeout(VALIDATOR_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n await cancelResponseBody(response);\n throw new Error(`W3C CSS validator returned HTTP status ${response.status}`);\n }\n\n const text = await readResponseText(response, MAX_VALIDATOR_RESPONSE_BYTES);\n\n // An empty upstream response is indeterminate and must never be presented as a clean result.\n if (!text || text.trim() === \"\") {\n throw new Error(\"W3C CSS validator returned an empty response\");\n }\n\n const data = JSON.parse(text) as {\n cssvalidation?: {\n errors?: Array<{\n line: number;\n message: string;\n context?: string;\n type?: string;\n }>;\n warnings?: Array<{\n line: number;\n message: string;\n context?: string;\n type?: string;\n }>;\n };\n };\n\n if (!data.cssvalidation || typeof data.cssvalidation !== \"object\") {\n throw new Error(\"W3C CSS validator returned an invalid response shape\");\n }\n\n const errors = data.cssvalidation.errors || [];\n return errors.slice(0, MAX_VALIDATION_MESSAGES).map((err) => {\n const message = err.message ? err.message.trim() : \"Unknown CSS validation error\";\n return {\n line: err.line || 0,\n type: \"error\",\n message,\n context: err.context || undefined,\n ...(isKnownCssValidatorLimitation(err.type, message)\n ? { compatibility: \"known-validator-limitation\" as const }\n : {}),\n };\n });\n } catch (error: unknown) {\n throw new Error(`CSS validation failed: ${getErrorMessage(error)}`);\n }\n}\n"]}
1
+ {"version":3,"file":"w3c-validator.js","sourceRoot":"","sources":["../src/w3c-validator.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n cancelResponseBody,\n getErrorMessage,\n readResponseText,\n} from \"./network.js\";\nimport { PACKAGE_VERSION } from \"./version.js\";\n\nexport interface W3CMessage {\n type: string;\n lastLine?: number;\n lastColumn?: number;\n firstLine?: number;\n firstColumn?: number;\n message: string;\n extract?: string;\n}\n\nexport interface CSSMessage {\n line: number;\n type: string;\n message: string;\n context?: string;\n compatibility?: \"known-validator-limitation\";\n}\n\nconst VALIDATOR_TIMEOUT_MS = 20_000;\nconst MAX_HTML_BYTES = 2_000_000;\nexport const MAX_CSS_VALIDATION_BYTES = 128_000;\nconst MAX_VALIDATOR_RESPONSE_BYTES = 5_000_000;\nconst USER_AGENT = `mcp-web-validator/${PACKAGE_VERSION} (+https://digestseo.com/validator-mcp/)`;\nexport const MAX_VALIDATION_MESSAGES = 200;\n\nfunction isKnownCssValidatorLimitation(type: string | undefined, message: string): boolean {\n if (type?.trim().toLowerCase() !== \"at-rule\") return false;\n const normalized = message.trim().toLowerCase().replace(/[“”\"'‘’]/g, \"\");\n return normalized === \"unrecognized at-rule @container\";\n}\n\nfunction assertContentSize(content: string, maxBytes: number, label: string): void {\n const size = Buffer.byteLength(content, \"utf8\");\n if (size > maxBytes) {\n throw new Error(`${label} exceeds the ${maxBytes}-byte validation limit`);\n }\n}\n\nfunction normalizeW3CMessage(message: unknown): W3CMessage {\n if (typeof message !== \"object\" || message === null) {\n throw new Error(\"W3C HTML validator returned a malformed message\");\n }\n\n const candidate = message as Record<string, unknown>;\n if (typeof candidate.type !== \"string\" || typeof candidate.message !== \"string\") {\n throw new Error(\"W3C HTML validator returned a malformed message\");\n }\n\n const normalized: W3CMessage = {\n type: candidate.type,\n message: candidate.message,\n };\n\n for (const field of [\"lastLine\", \"lastColumn\", \"firstLine\", \"firstColumn\"] as const) {\n if (typeof candidate[field] === \"number\" && Number.isInteger(candidate[field])) {\n normalized[field] = candidate[field];\n }\n }\n\n if (typeof candidate.extract === \"string\") {\n normalized.extract = candidate.extract;\n }\n\n return normalized;\n}\n\n/**\n * Validates HTML using the W3C Nu HTML Checker API\n */\nexport async function validateHtmlContent(htmlContent: string): Promise<W3CMessage[]> {\n const url = \"https://validator.w3.org/nu/?out=json\";\n\n try {\n assertContentSize(htmlContent, MAX_HTML_BYTES, \"HTML content\");\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"text/html; charset=utf-8\",\n \"User-Agent\": USER_AGENT,\n },\n body: htmlContent,\n redirect: \"error\",\n signal: AbortSignal.timeout(VALIDATOR_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n await cancelResponseBody(response);\n throw new Error(`W3C HTML validator returned HTTP status ${response.status}`);\n }\n\n const text = await readResponseText(response, MAX_VALIDATOR_RESPONSE_BYTES);\n const data = JSON.parse(text) as { messages?: unknown };\n if (data.messages === undefined) {\n throw new Error(\"W3C HTML validator returned an invalid response shape\");\n }\n if (!Array.isArray(data.messages)) {\n throw new Error(\"W3C HTML validator returned an invalid response shape\");\n }\n return data.messages\n .map(normalizeW3CMessage)\n .slice(0, MAX_VALIDATION_MESSAGES);\n } catch (error: unknown) {\n throw new Error(`HTML validation failed: ${getErrorMessage(error)}`);\n }\n}\n\n/**\n * Validates CSS using the W3C Jigsaw CSS Validator API\n */\nexport async function validateCssContent(cssContent: string): Promise<CSSMessage[]> {\n const url = \"https://jigsaw.w3.org/css-validator/validator\";\n\n try {\n assertContentSize(cssContent, MAX_CSS_VALIDATION_BYTES, \"CSS content\");\n const form = new FormData();\n form.set(\"text\", cssContent);\n form.set(\"output\", \"json\");\n form.set(\"warning\", \"0\");\n form.set(\"profile\", \"css3svg\");\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n \"User-Agent\": USER_AGENT,\n },\n body: form,\n redirect: \"error\",\n signal: AbortSignal.timeout(VALIDATOR_TIMEOUT_MS),\n });\n\n if (!response.ok) {\n await cancelResponseBody(response);\n throw new Error(`W3C CSS validator returned HTTP status ${response.status}`);\n }\n\n const text = await readResponseText(response, MAX_VALIDATOR_RESPONSE_BYTES);\n\n // An empty upstream response is indeterminate and must never be presented as a clean result.\n if (!text || text.trim() === \"\") {\n throw new Error(\"W3C CSS validator returned an empty response\");\n }\n\n const data = JSON.parse(text) as {\n cssvalidation?: {\n errors?: Array<{\n line: number;\n message: string;\n context?: string;\n type?: string;\n }>;\n warnings?: Array<{\n line: number;\n message: string;\n context?: string;\n type?: string;\n }>;\n };\n };\n\n if (!data.cssvalidation || typeof data.cssvalidation !== \"object\") {\n throw new Error(\"W3C CSS validator returned an invalid response shape\");\n }\n\n const errors = data.cssvalidation.errors || [];\n return errors.slice(0, MAX_VALIDATION_MESSAGES).map((err) => {\n const message = err.message ? err.message.trim() : \"Unknown CSS validation error\";\n return {\n line: err.line || 0,\n type: \"error\",\n message,\n context: err.context || undefined,\n ...(isKnownCssValidatorLimitation(err.type, message)\n ? { compatibility: \"known-validator-limitation\" as const }\n : {}),\n };\n });\n } catch (error: unknown) {\n throw new Error(`CSS validation failed: ${getErrorMessage(error)}`);\n }\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-web-validator",
3
- "version": "1.3.3",
3
+ "version": "1.3.5",
4
4
  "description": "W3C HTML/CSS Validator and Technical SEO Audit MCP Server for AI coding assistants.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {