Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .htmlvalidate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
{
Expand Down
14 changes: 5 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
65 changes: 0 additions & 65 deletions test/build-structured-data-validate.mjs

This file was deleted.

58 changes: 0 additions & 58 deletions test/fixtures-structured-data-should-fail.mjs

This file was deleted.

23 changes: 23 additions & 0 deletions test/fixtures/required-results.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "<head> is missing <link rel=\"canonical\" ...>",
"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",

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good, if this is the actual result

"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",
Expand Down
3 changes: 3 additions & 0 deletions test/plugin.html-validate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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: {
Expand All @@ -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",
},
},
},
Expand Down
176 changes: 176 additions & 0 deletions test/plugin.html-validate.structured-data.mjs
Original file line number Diff line number Diff line change
@@ -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('<script type="application/ld+json">')) {
startLine = i + 1; // Start after the opening tag
inScript = true;
} else if (inScript && line.includes("</script>")) {
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";

Copilot AI Sep 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded /tmp directory may not exist on all systems (e.g., Windows). Use os.tmpdir() from the Node.js os module for cross-platform compatibility.

Copilot uses AI. Check for mistakes.
const tempFileName = `structured-data-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.html`;

Copilot AI Sep 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use substring() instead of the deprecated substr() method. Replace .substr(2, 9) with .substring(2, 11).

Suggested change
const tempFileName = `structured-data-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.html`;
const tempFileName = `structured-data-${Date.now()}-${Math.random().toString(36).substring(2, 11)}.html`;

Copilot uses AI. Check for mistakes.
const tempFilePath = path.join(tempDir, tempFileName);

try {
// Create minimal HTML with just the JSON-LD script
const tempHtml = `<!DOCTYPE html>
<html>
<head>
<title>Structured Data Test</title>
</head>
<body>
<script type="application/ld+json">
${content}
</script>
</body>
</html>`;

fs.writeFileSync(tempFilePath, tempHtml);

// Run structured-data-testing-tool on the temporary file
const result = execSync(`yarn dlx structured-data-testing-tool --file "${tempFilePath}"`, {

Copilot AI Sep 16, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tempFilePath variable is not properly escaped for shell execution, which could lead to command injection if the path contains special characters. Use proper argument escaping or pass arguments as an array to avoid shell injection vulnerabilities.

Copilot uses AI. Check for mistakes.
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}`,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good

});
} 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}`,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good

});
} finally {
// Clean up temporary file
try {
if (fs.existsSync(tempFilePath)) {
fs.unlinkSync(tempFilePath);
}
} catch (cleanupError) {
// Ignore cleanup errors
}
}
}
}
Loading