diff --git a/.htmlvalidate.mjs b/.htmlvalidate.mjs index 735199a..350fd96 100644 --- a/.htmlvalidate.mjs +++ b/.htmlvalidate.mjs @@ -30,6 +30,7 @@ export default defineConfig({ "pacific-medical-training/latest-packages": "error", "pacific-medical-training/https-links": "error", "pacific-medical-training/internal-links": "error", + "pacific-medical-training/structured-data": "error", "wcag/h37": [ "error", { diff --git a/README.md b/README.md index e03ed2b..2821ff4 100644 --- a/README.md +++ b/README.md @@ -70,15 +70,11 @@ Perform website testing (you must have already [built the site](#build-the-site) yarn test ``` -#### Structured Data Testing - -Test structured data (JSON-LD) validation specifically: - -```sh -yarn test-structured-data -``` - -This validates that any `application/ld+json` scripts in `build/**/*.html` files have correct schema.org formats and valid JSON syntax. +This includes validation of: +- HTML structure and accessibility +- External and internal links +- Structured data (JSON-LD) validation using schema.org standards +- Content quality checks ## Notes for VS Code diff --git a/package.json b/package.json index 1a76422..e298a1d 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,8 @@ "structured-data-testing-tool": "^4.5.0" }, "scripts": { - "test": "yarn node test/build-html-validate.mjs && yarn node test/dirty-file-paths-checker.mjs && yarn node test/build-structured-data-validate.mjs", - "test-test": "yarn node test/fixtures-html-validate-should-fail.mjs && yarn node test/fixtures-structured-data-should-fail.mjs", - "test-structured-data": "yarn node test/build-structured-data-validate.mjs", + "test": "yarn node test/build-html-validate.mjs && yarn node test/dirty-file-paths-checker.mjs", + "test-test": "yarn node test/fixtures-html-validate-should-fail.mjs", "lint": "yarn prettier-check && yarn markdownlint-check", "lint-fix": "yarn prettier-fix && yarn markdownlint-fix", "generate-sitemap": "node scripts/generate-sitemap.mjs", diff --git a/test/build-structured-data-validate.mjs b/test/build-structured-data-validate.mjs deleted file mode 100644 index 87b0b17..0000000 --- a/test/build-structured-data-validate.mjs +++ /dev/null @@ -1,65 +0,0 @@ -import { glob } from "glob"; -import { execSync } from "child_process"; -import fs from "fs"; -import path from "path"; - -// Find and sort all HTML files in the 'build' directory -const targets = glob.sync("build/**/*.html").sort(); - -if (targets.length === 0) { - console.log("โš ๏ธ No HTML files found in build directory"); - console.log(" Make sure to build the site first"); - process.exit(0); -} - -console.log(`๐Ÿงช Validating structured data in ${targets.length} files...`); - -let hasErrors = false; - -// Validate each target file -for (const target of targets) { - try { - // Use structured-data-testing-tool CLI to test the file - const result = execSync(`yarn dlx structured-data-testing-tool --file "${target}"`, { - encoding: "utf8", - stdio: "pipe", - }); - - // Check if there are JSON-LD parse errors or failed tests in the output - if (result.includes("Error in jsonld parse") || (result.includes("Failed:") && !result.includes("Failed: 0"))) { - console.log("โŒ " + target); - console.log(result); - hasErrors = true; - } else { - console.log("โœ… " + target); - // Show a summary of structured data found - const lines = result.split("\n"); - const schemaLine = lines.find((line) => line.includes("Schema.org schemas:")); - if (schemaLine && !schemaLine.includes("Schema.org schemas: 0")) { - console.log(` ๐Ÿ“‹ ${schemaLine.trim()}`); - } - // Only show detailed output if there are warnings - if (result.includes("Warnings:") && !result.includes("Warnings: 0")) { - console.log("โš ๏ธ Warnings found:"); - console.log(result); - } - } - } catch (error) { - console.log("โŒ " + target); - console.log("โš ๏ธ Error testing file:", error.message); - if (error.stdout) { - console.log(error.stdout); - } - if (error.stderr) { - console.error(error.stderr); - } - hasErrors = true; - } -} - -if (hasErrors) { - console.log("โŒ Some tests failed."); - process.exit(1); -} else { - console.log("โœจ All tests passed!\n"); -} diff --git a/test/fixtures-structured-data-should-fail.mjs b/test/fixtures-structured-data-should-fail.mjs deleted file mode 100644 index 88dd473..0000000 --- a/test/fixtures-structured-data-should-fail.mjs +++ /dev/null @@ -1,58 +0,0 @@ -import { execSync } from "child_process"; -import path from "path"; - -console.log("๐Ÿงช Testing structured data fixtures"); - -const fixtures = [ - { - file: "test/fixtures/valid-jsonld.html", - shouldPass: true, - description: "valid JSON-LD", - }, - { - file: "test/fixtures/invalid-jsonld.html", - shouldPass: false, - description: "invalid JSON-LD (syntax error)", - }, -]; - -let allTestsPassed = true; - -for (const fixture of fixtures) { - try { - const result = execSync(`yarn dlx structured-data-testing-tool --file "${fixture.file}"`, { - encoding: "utf8", - stdio: "pipe", - }); - - const hasError = - result.includes("Error in jsonld parse") || (result.includes("Failed:") && !result.includes("Failed: 0")); - - const actuallyPassed = !hasError; - - if (actuallyPassed === fixture.shouldPass) { - console.log(`โœ… ${fixture.file}: ${fixture.description} - ${actuallyPassed ? "passed" : "failed"} as expected`); - } else { - console.error( - `โŒ ${fixture.file}: ${fixture.description} - expected ${fixture.shouldPass ? "pass" : "fail"} but got ${actuallyPassed ? "pass" : "fail"}`, - ); - console.error("Output:", result); - allTestsPassed = false; - } - } catch (error) { - // execSync throws on non-zero exit codes, but structured-data-testing-tool doesn't always exit with error codes - console.error(`โŒ ${fixture.file}: ${fixture.description} - execution error`); - console.error("Error:", error.message); - if (error.stdout) { - console.error("Stdout:", error.stdout); - } - allTestsPassed = false; - } -} - -if (allTestsPassed) { - console.log("โœจ All fixtures produced expected results!\n"); -} else { - console.error("โŒ Some fixture tests failed."); - process.exit(1); -} diff --git a/test/fixtures/required-results.json b/test/fixtures/required-results.json index 3944351..a166bc7 100644 --- a/test/fixtures/required-results.json +++ b/test/fixtures/required-results.json @@ -166,6 +166,29 @@ "ruleUrl": "https://html-validate.org/rules/wcag/h37.html" } ], + "test/fixtures/valid-jsonld.html": [ + { + "ruleId": "pacific-medical-training/canonical-link", + "severity": 2, + "message": " is missing ", + "size": 0, + "selector": null, + "ruleUrl": "https://github.com/fulldecent/github-pages-template/#canonical" + } + ], + "test/fixtures/invalid-jsonld.html": [ + { + "ruleId": "pacific-medical-training/structured-data", + "severity": 2, + "message": "JSON-LD parse error: SyntaxError: Expected property name or '}' in JSON at position 11", + "offset": 279, + "line": 11, + "column": 39, + "size": 1, + "selector": "html > body > script", + "ruleUrl": "https://github.com/fulldecent/github-pages-template/#structured-data" + } + ], "test/fixtures/directory-path-links-test.html": [ { "ruleId": "pacific-medical-training/internal-links", diff --git a/test/plugin.html-validate.mjs b/test/plugin.html-validate.mjs index 57e4ed9..cdbd0ed 100644 --- a/test/plugin.html-validate.mjs +++ b/test/plugin.html-validate.mjs @@ -7,6 +7,7 @@ import CanonicalLinkRule from "./plugin.html-validate.canonical-link.mjs"; import LatestPackagesRules from "./plugin.html-validate.latest-packages.mjs"; import EnsureHttpsRules from "./plugin.html-validate.https-links.mjs"; import CheckInternalLinks from "./plugin.html-validate.internal-links.mjs"; +import StructuredDataRule from "./plugin.html-validate.structured-data.mjs"; export default definePlugin({ name: "pacific-medical-training", @@ -18,6 +19,7 @@ export default definePlugin({ "pacific-medical-training/latest-packages": LatestPackagesRules, "pacific-medical-training/https-links": EnsureHttpsRules, "pacific-medical-training/internal-links": CheckInternalLinks, + "pacific-medical-training/structured-data": StructuredDataRule, }, configs: { recommended: { @@ -29,6 +31,7 @@ export default definePlugin({ "pacific-medical-training/latest-packages": "error", "pacific-medical-training/https-links": "error", "pacific-medical-training/internal-links": "error", + "pacific-medical-training/structured-data": "error", }, }, }, diff --git a/test/plugin.html-validate.structured-data.mjs b/test/plugin.html-validate.structured-data.mjs new file mode 100644 index 0000000..46307c5 --- /dev/null +++ b/test/plugin.html-validate.structured-data.mjs @@ -0,0 +1,176 @@ +import { Rule } from "html-validate"; +import { execSync } from "child_process"; +import fs from "fs"; +import path from "path"; + +export default class StructuredDataRule extends Rule { + documentation() { + return { + description: "Validate JSON-LD structured data using structured-data-testing-tool", + url: "https://github.com/fulldecent/github-pages-template/#structured-data", + }; + } + + setup() { + this.on("tag:ready", this.tagReady.bind(this)); + } + + tagReady({ target }) { + if (target.tagName === "script") { + const type = target.getAttribute("type")?.value; + + // Only process script tags with type="application/ld+json" + if (type === "application/ld+json") { + this.validateJsonLd(target); + } + } + } + + validateJsonLd(scriptElement) { + // Try to read the file content directly and extract the script + if (scriptElement.location && scriptElement.location.filename) { + try { + const fileContent = fs.readFileSync(scriptElement.location.filename, "utf8"); + const lines = fileContent.split("\n"); + + // Find script tag boundaries + let startLine = -1; + let endLine = -1; + let inScript = false; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.includes('")) { + endLine = i; + break; + } + } + + if (startLine >= 0 && endLine >= 0) { + const scriptContent = lines.slice(startLine, endLine).join("\n").trim(); + + if (scriptContent) { + this.testStructuredData(scriptContent, scriptElement); + return; + } + } + } catch (error) { + this.report({ + node: scriptElement, + message: `Error reading file for structured data validation: ${error.message}`, + }); + return; + } + } + + this.report({ + node: scriptElement, + message: "JSON-LD script tag is empty or cannot be read", + }); + } + + testStructuredData(content, scriptElement) { + // Create a temporary HTML file with just this JSON-LD script + const tempDir = "/tmp"; + const tempFileName = `structured-data-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.html`; + const tempFilePath = path.join(tempDir, tempFileName); + + try { + // Create minimal HTML with just the JSON-LD script + const tempHtml = ` + + + Structured Data Test + + + + +`; + + fs.writeFileSync(tempFilePath, tempHtml); + + // Run structured-data-testing-tool on the temporary file + const result = execSync(`yarn dlx structured-data-testing-tool --file "${tempFilePath}"`, { + encoding: "utf8", + stdio: "pipe", + }); + + // Check for errors in the output + if (result.includes("Error in jsonld parse")) { + // Extract the specific parse error message + const errorMatch = result.match(/Error in jsonld parse - (.+)/); + const errorDetail = errorMatch ? errorMatch[1] : "Unknown parse error"; + this.report({ + node: scriptElement, + message: `JSON-LD parse error: ${errorDetail}`, + }); + } else if (result.includes("Failed:") && !result.includes("Failed: 0")) { + // Extract failed test count and any test details + const failedMatch = result.match(/Failed: (\d+)/); + const failedCount = failedMatch ? failedMatch[1] : "some"; + + // Try to extract specific test failure information + const testFailures = []; + const lines = result.split("\n"); + let inTestSection = false; + + for (const line of lines) { + if (line.includes("Tests")) { + inTestSection = true; + continue; + } + if (line.includes("Statistics")) { + inTestSection = false; + break; + } + if (inTestSection && line.includes("โœ—")) { + testFailures.push(line.trim()); + } + } + + let message = `Structured data validation failed: ${failedCount} test(s) failed`; + if (testFailures.length > 0) { + message += ` (${testFailures.join(", ")})`; + } + + this.report({ + node: scriptElement, + message: message, + }); + } + // Note: We don't report warnings as errors, only actual failures + } catch (error) { + // Extract more specific error information + let errorMessage = error.message; + + // If there's stdout/stderr, try to extract useful information + if (error.stdout && error.stdout.includes("Error in jsonld parse")) { + const errorMatch = error.stdout.match(/Error in jsonld parse - (.+)/); + const parseError = errorMatch ? errorMatch[1] : "Unknown parse error"; + errorMessage = `JSON-LD parse error: ${parseError}`; + } else if (error.stderr) { + // Use stderr if available for more detailed error info + errorMessage = error.stderr.trim() || errorMessage; + } + + this.report({ + node: scriptElement, + message: `Structured data testing error: ${errorMessage}`, + }); + } finally { + // Clean up temporary file + try { + if (fs.existsSync(tempFilePath)) { + fs.unlinkSync(tempFilePath); + } + } catch (cleanupError) { + // Ignore cleanup errors + } + } + } +}