Files
CompanySite/.agents/seo-specialist/validate-structured-data.js
T
Clintchiz d402256547
Deploy to Production / Build & Verify (push) Failing after 5m56s
Ping Search Engines / Notify Search Engines (push) Successful in 2s
Deploy to Production / Pre-Deploy Tests (push) Has been skipped
Deploy to Production / Deploy to Railway (push) Has been skipped
Deploy to Production / Deploy to Render (push) Has been skipped
Deploy to Production / Deploy to VPS (PM2) (push) Has been skipped
Deploy to Production / Deploy to Fly.io (push) Has been skipped
Deploy to Production / Post-Deploy Verification (push) Has been skipped
Deploy to Production / Notify on Failure (push) Successful in 2s
E2E Test Suite / Critical User Journeys (push) Has been skipped
E2E Test Suite / API Integration Tests (push) Has been skipped
E2E Test Suite / Smoke Tests (P0) (push) Failing after 11m26s
E2E Test Suite / Form Interaction Tests (push) Failing after 11m42s
E2E Test Suite / Destructive & Chaos Tests (push) Failing after 12m2s
E2E Test Suite / Cross-Browser Regression (chromium) (push) Failing after 16m14s
E2E Test Suite / Cross-Browser Regression (webkit) (push) Failing after 17m45s
E2E Test Suite / Cross-Browser Regression (firefox) (push) Failing after 25m23s
E2E Test Suite / Security Header Tests (push) Failing after 7m55s
E2E Test Suite / Test Report Summary (push) Failing after 20s
E2E Test Suite / Mobile Device Tests (push) Failing after 2h49m9s
Uptime Monitor / Health & Response Time (push) Failing after 2s
Uptime Monitor / SSL Certificate (push) Successful in 2s
Uptime Monitor / Send Alerts (push) Failing after 3s
Uptime Monitor / Record Uptime Success (push) Has been skipped
First Init
2026-03-21 16:46:46 +05:30

485 lines
12 KiB
JavaScript

/**
* Structured Data Validation Script
*
* Validates JSON-LD structured data in built HTML files
* Run: node .agents/seo-specialist/validate-structured-data.js
*/
import { readFileSync, readdirSync, statSync } from 'fs';
import { join, extname } from 'path';
const distDir = './dist';
const errors = [];
const warnings = [];
const schemaStats = {};
// ANSI color codes for terminal output
const colors = {
reset: '\x1b[0m',
green: '\x1b[32m',
yellow: '\x1b[33m',
red: '\x1b[31m',
cyan: '\x1b[36m',
bold: '\x1b[1m'
};
function log(message, color = 'reset') {
console.log(`${colors[color]}${message}${colors.reset}`);
}
/**
* Recursively find all HTML files in dist directory
*/
function findHtmlFiles(dir) {
const files = [];
try {
const items = readdirSync(dir);
for (const item of items) {
const fullPath = join(dir, item);
const stat = statSync(fullPath);
if (stat.isDirectory()) {
files.push(...findHtmlFiles(fullPath));
} else if (extname(item) === '.html') {
files.push(fullPath);
}
}
} catch (err) {
errors.push(`Error reading directory ${dir}: ${err.message}`);
}
return files;
}
/**
* Extract and validate JSON-LD schemas from HTML content
*/
function extractSchemas(html, filePath) {
const schemaRegex = /<script type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/gi;
const matches = [...html.matchAll(schemaRegex)];
const schemas = [];
for (const match of matches) {
try {
const jsonContent = match[1].trim();
const schema = JSON.parse(jsonContent);
schemas.push(schema);
// Track schema types
const type = schema['@type'];
if (type) {
schemaStats[type] = (schemaStats[type] || 0) + 1;
}
} catch (err) {
errors.push({
file: filePath,
type: 'PARSE_ERROR',
message: `Invalid JSON-LD: ${err.message}`,
content: match[1].substring(0, 100) + '...'
});
}
}
return schemas;
}
/**
* Validate Organization schema
*/
function validateOrganization(schema, filePath) {
const required = ['@context', '@type', 'name', 'url'];
const recommended = ['logo', 'description', 'contactPoint', 'sameAs', 'address'];
// Check required fields
for (const field of required) {
if (!schema[field]) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'Organization',
field
});
}
}
// Check recommended fields
for (const field of recommended) {
if (!schema[field]) {
warnings.push({
file: filePath,
type: 'MISSING_RECOMMENDED',
schema: 'Organization',
field
});
}
}
// Validate logo is ImageObject
if (schema.logo && typeof schema.logo === 'object') {
if (!schema.logo['@type'] || schema.logo['@type'] !== 'ImageObject') {
warnings.push({
file: filePath,
type: 'INVALID_TYPE',
schema: 'Organization',
message: 'Logo should be an ImageObject'
});
}
}
}
/**
* Validate BlogPosting schema
*/
function validateBlogPosting(schema, filePath) {
const required = ['@context', '@type', 'headline', 'author', 'datePublished', 'publisher'];
const recommended = ['image', 'dateModified', 'mainEntityOfPage', 'keywords'];
for (const field of required) {
if (!schema[field]) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'BlogPosting',
field
});
}
}
for (const field of recommended) {
if (!schema[field]) {
warnings.push({
file: filePath,
type: 'MISSING_RECOMMENDED',
schema: 'BlogPosting',
field
});
}
}
// Validate author is Person
if (schema.author && typeof schema.author === 'object') {
if (!schema.author['@type'] || schema.author['@type'] !== 'Person') {
errors.push({
file: filePath,
type: 'INVALID_TYPE',
schema: 'BlogPosting',
message: 'Author must be a Person schema'
});
}
}
// Validate publisher is Organization with logo
if (schema.publisher && typeof schema.publisher === 'object') {
if (!schema.publisher['@type'] || schema.publisher['@type'] !== 'Organization') {
errors.push({
file: filePath,
type: 'INVALID_TYPE',
schema: 'BlogPosting',
message: 'Publisher must be an Organization schema'
});
}
if (!schema.publisher.logo) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'BlogPosting',
message: 'Publisher must have a logo'
});
}
}
}
/**
* Validate BreadcrumbList schema
*/
function validateBreadcrumbList(schema, filePath) {
if (!schema.itemListElement || !Array.isArray(schema.itemListElement)) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'BreadcrumbList',
field: 'itemListElement (must be array)'
});
return;
}
schema.itemListElement.forEach((item, index) => {
if (!item['@type'] || item['@type'] !== 'ListItem') {
errors.push({
file: filePath,
type: 'INVALID_TYPE',
schema: 'BreadcrumbList',
message: `Item ${index} must be ListItem`
});
}
if (!item.position) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'BreadcrumbList',
message: `Item ${index} missing position`
});
}
if (!item.name) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'BreadcrumbList',
message: `Item ${index} missing name`
});
}
});
}
/**
* Validate FAQPage schema
*/
function validateFAQPage(schema, filePath) {
if (!schema.mainEntity || !Array.isArray(schema.mainEntity)) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'FAQPage',
field: 'mainEntity (must be array)'
});
return;
}
schema.mainEntity.forEach((item, index) => {
if (!item['@type'] || item['@type'] !== 'Question') {
errors.push({
file: filePath,
type: 'INVALID_TYPE',
schema: 'FAQPage',
message: `Item ${index} must be Question`
});
}
if (!item.name) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'FAQPage',
message: `Question ${index} missing name`
});
}
if (!item.acceptedAnswer) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: 'FAQPage',
message: `Question ${index} missing acceptedAnswer`
});
} else if (!item.acceptedAnswer['@type'] || item.acceptedAnswer['@type'] !== 'Answer') {
errors.push({
file: filePath,
type: 'INVALID_TYPE',
schema: 'FAQPage',
message: `Question ${index} acceptedAnswer must be Answer`
});
}
});
}
/**
* Validate schema based on type
*/
function validateSchema(schema, filePath) {
const type = schema['@type'];
switch (type) {
case 'Organization':
validateOrganization(schema, filePath);
break;
case 'BlogPosting':
validateBlogPosting(schema, filePath);
break;
case 'BreadcrumbList':
validateBreadcrumbList(schema, filePath);
break;
case 'FAQPage':
validateFAQPage(schema, filePath);
break;
// Other schema types can pass through without specific validation
default:
// Just check for @context and @type
if (!schema['@context']) {
errors.push({
file: filePath,
type: 'MISSING_REQUIRED',
schema: type,
field: '@context'
});
}
}
}
/**
* Check for Open Graph tags
*/
function validateOpenGraph(html, filePath) {
const requiredOgTags = ['og:title', 'og:description', 'og:url', 'og:image'];
for (const tag of requiredOgTags) {
const regex = new RegExp(`<meta\\s+property="${tag}"`, 'i');
if (!regex.test(html)) {
errors.push({
file: filePath,
type: 'MISSING_OG_TAG',
tag
});
}
}
}
/**
* Check for Twitter Card tags
*/
function validateTwitterCards(html, filePath) {
const requiredTwitterTags = ['twitter:card', 'twitter:title', 'twitter:description', 'twitter:image'];
for (const tag of requiredTwitterTags) {
const regex = new RegExp(`<meta\\s+name="${tag}"`, 'i');
if (!regex.test(html)) {
errors.push({
file: filePath,
type: 'MISSING_TWITTER_TAG',
tag
});
}
}
}
/**
* Main validation function
*/
function validateFile(filePath) {
try {
const html = readFileSync(filePath, 'utf-8');
// Extract and validate JSON-LD schemas
const schemas = extractSchemas(html, filePath);
for (const schema of schemas) {
validateSchema(schema, filePath);
}
// Validate Open Graph
validateOpenGraph(html, filePath);
// Validate Twitter Cards
validateTwitterCards(html, filePath);
return schemas.length;
} catch (err) {
errors.push({
file: filePath,
type: 'FILE_ERROR',
message: err.message
});
return 0;
}
}
/**
* Print validation report
*/
function printReport(filesProcessed, totalSchemas) {
console.log('\n' + '='.repeat(80));
log('STRUCTURED DATA VALIDATION REPORT', 'bold');
console.log('='.repeat(80) + '\n');
log(`Files Processed: ${filesProcessed}`, 'cyan');
log(`Total Schemas Found: ${totalSchemas}`, 'cyan');
console.log();
// Schema type breakdown
log('Schema Types:', 'bold');
for (const [type, count] of Object.entries(schemaStats).sort((a, b) => b[1] - a[1])) {
log(` ${type}: ${count}`, 'cyan');
}
console.log();
// Errors
if (errors.length > 0) {
log(`❌ ERRORS (${errors.length}):`, 'red');
errors.forEach((error, index) => {
console.log(`\n${index + 1}. ${error.file || 'Unknown'}`);
log(` Type: ${error.type}`, 'red');
if (error.schema) log(` Schema: ${error.schema}`, 'red');
if (error.field) log(` Field: ${error.field}`, 'red');
if (error.tag) log(` Tag: ${error.tag}`, 'red');
if (error.message) log(` Message: ${error.message}`, 'red');
if (error.content) log(` Content: ${error.content}`, 'red');
});
console.log();
}
// Warnings
if (warnings.length > 0) {
log(`⚠️ WARNINGS (${warnings.length}):`, 'yellow');
warnings.forEach((warning, index) => {
console.log(`\n${index + 1}. ${warning.file || 'Unknown'}`);
log(` Type: ${warning.type}`, 'yellow');
if (warning.schema) log(` Schema: ${warning.schema}`, 'yellow');
if (warning.field) log(` Field: ${warning.field}`, 'yellow');
if (warning.message) log(` Message: ${warning.message}`, 'yellow');
});
console.log();
}
// Summary
console.log('='.repeat(80));
if (errors.length === 0 && warnings.length === 0) {
log('✅ ALL VALIDATIONS PASSED!', 'green');
} else if (errors.length === 0) {
log('✅ NO ERRORS (but some warnings)', 'green');
} else {
log('❌ VALIDATION FAILED', 'red');
}
console.log('='.repeat(80) + '\n');
// Exit code
process.exit(errors.length > 0 ? 1 : 0);
}
/**
* Main execution
*/
function main() {
log('\n🔍 Starting Structured Data Validation...\n', 'cyan');
// Check if dist directory exists
try {
statSync(distDir);
} catch (err) {
log('❌ Error: dist directory not found. Run `npm run build` first.', 'red');
process.exit(1);
}
// Find all HTML files
const htmlFiles = findHtmlFiles(distDir);
log(`Found ${htmlFiles.length} HTML files to validate\n`, 'cyan');
// Validate each file
let totalSchemas = 0;
for (const file of htmlFiles) {
const schemaCount = validateFile(file);
totalSchemas += schemaCount;
log(`✓ ${file} (${schemaCount} schemas)`, 'green');
}
// Print report
printReport(htmlFiles.length, totalSchemas);
}
// Run validation
main();