Files
CompanySite/scripts/validate-schema.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

342 lines
11 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* Schema Validation Script
*
* Validates JSON-LD structured data on all pages
* Checks for common issues and provides recommendations
*
* Usage: node scripts/validate-schema.js
*/
import { JSDOM } from 'jsdom';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const COLORS = {
reset: '\x1b[0m',
red: '\x1b[31m',
green: '\x1b[32m',
yellow: '\x1b[33m',
blue: '\x1b[34m',
cyan: '\x1b[36m',
};
const log = {
success: (msg) => console.log(`${COLORS.green}${COLORS.reset} ${msg}`),
error: (msg) => console.log(`${COLORS.red}${COLORS.reset} ${msg}`),
warning: (msg) => console.log(`${COLORS.yellow}${COLORS.reset} ${msg}`),
info: (msg) => console.log(`${COLORS.blue}${COLORS.reset} ${msg}`),
section: (msg) => console.log(`\n${COLORS.cyan}${msg}${COLORS.reset}\n`),
};
/**
* Extract JSON-LD scripts from HTML
*/
function extractJsonLd(html) {
const dom = new JSDOM(html);
const scripts = dom.window.document.querySelectorAll('script[type="application/ld+json"]');
return Array.from(scripts).map(script => {
try {
return JSON.parse(script.textContent);
} catch (e) {
log.error(`Failed to parse JSON-LD: ${e.message}`);
return null;
}
}).filter(Boolean);
}
/**
* Validate required fields for each schema type
*/
function validateSchema(schema) {
const issues = [];
const warnings = [];
if (!schema['@context']) {
issues.push('Missing @context');
}
if (!schema['@type']) {
issues.push('Missing @type');
}
const type = schema['@type'];
// Validate based on type
switch (type) {
case 'Organization':
if (!schema.name) issues.push('Organization: Missing name');
if (!schema.url) issues.push('Organization: Missing url');
if (!schema.logo) warnings.push('Organization: Consider adding logo');
if (!schema.contactPoint) warnings.push('Organization: Consider adding contactPoint');
break;
case 'BlogPosting':
if (!schema.headline) issues.push('BlogPosting: Missing headline');
if (!schema.author) issues.push('BlogPosting: Missing author');
if (!schema.datePublished) issues.push('BlogPosting: Missing datePublished');
if (!schema.publisher) issues.push('BlogPosting: Missing publisher');
if (!schema.image) warnings.push('BlogPosting: Consider adding image');
break;
case 'BreadcrumbList':
if (!schema.itemListElement || !Array.isArray(schema.itemListElement)) {
issues.push('BreadcrumbList: Missing or invalid itemListElement');
} else {
schema.itemListElement.forEach((item, idx) => {
if (!item.position) issues.push(`BreadcrumbList: Item ${idx} missing position`);
if (!item.name) issues.push(`BreadcrumbList: Item ${idx} missing name`);
if (!item.item) issues.push(`BreadcrumbList: Item ${idx} missing item URL`);
});
}
break;
case 'WebPage':
if (!schema.name) issues.push('WebPage: Missing name');
if (!schema.url) issues.push('WebPage: Missing url');
if (!schema.description) warnings.push('WebPage: Consider adding description');
break;
case 'FAQPage':
if (!schema.mainEntity || !Array.isArray(schema.mainEntity)) {
issues.push('FAQPage: Missing or invalid mainEntity');
} else {
schema.mainEntity.forEach((item, idx) => {
if (item['@type'] !== 'Question') {
issues.push(`FAQPage: Item ${idx} should be type Question`);
}
if (!item.name) issues.push(`FAQPage: Question ${idx} missing name`);
if (!item.acceptedAnswer) {
issues.push(`FAQPage: Question ${idx} missing acceptedAnswer`);
}
});
}
break;
case 'Service':
if (!schema.name) issues.push('Service: Missing name');
if (!schema.description) issues.push('Service: Missing description');
if (!schema.provider) warnings.push('Service: Consider adding provider');
break;
case 'WebSite':
if (!schema.name) issues.push('WebSite: Missing name');
if (!schema.url) issues.push('WebSite: Missing url');
if (!schema.potentialAction) warnings.push('WebSite: Consider adding search action');
break;
}
return { issues, warnings };
}
/**
* Validate Open Graph tags
*/
function validateOpenGraph(html) {
const dom = new JSDOM(html);
const doc = dom.window.document;
const issues = [];
const warnings = [];
const requiredOgTags = ['og:title', 'og:description', 'og:image', 'og:url', 'og:type'];
requiredOgTags.forEach(tag => {
const meta = doc.querySelector(`meta[property="${tag}"]`);
if (!meta) {
issues.push(`Missing required Open Graph tag: ${tag}`);
}
});
const ogImage = doc.querySelector('meta[property="og:image"]');
if (ogImage) {
const imageUrl = ogImage.getAttribute('content');
if (!imageUrl.startsWith('http')) {
warnings.push('og:image should be an absolute URL');
}
if (!doc.querySelector('meta[property="og:image:width"]')) {
warnings.push('Consider adding og:image:width');
}
if (!doc.querySelector('meta[property="og:image:height"]')) {
warnings.push('Consider adding og:image:height');
}
}
return { issues, warnings };
}
/**
* Validate Twitter Card tags
*/
function validateTwitterCard(html) {
const dom = new JSDOM(html);
const doc = dom.window.document;
const issues = [];
const warnings = [];
const requiredTwitterTags = ['twitter:card', 'twitter:title', 'twitter:description', 'twitter:image'];
requiredTwitterTags.forEach(tag => {
const meta = doc.querySelector(`meta[name="${tag}"]`);
if (!meta) {
issues.push(`Missing required Twitter Card tag: ${tag}`);
}
});
const twitterCard = doc.querySelector('meta[name="twitter:card"]');
if (twitterCard && twitterCard.getAttribute('content') === 'summary_large_image') {
const twitterImage = doc.querySelector('meta[name="twitter:image"]');
if (twitterImage) {
const imageUrl = twitterImage.getAttribute('content');
if (!imageUrl.startsWith('http')) {
warnings.push('twitter:image should be an absolute URL');
}
}
}
return { issues, warnings };
}
/**
* Main validation function
*/
async function validatePage(filePath, pageName) {
log.section(`Validating: ${pageName}`);
try {
const html = fs.readFileSync(filePath, 'utf-8');
// Extract and validate JSON-LD
const schemas = extractJsonLd(html);
if (schemas.length === 0) {
log.warning('No JSON-LD structured data found');
} else {
log.info(`Found ${schemas.length} JSON-LD schema(s)`);
schemas.forEach((schema, idx) => {
console.log(`\n Schema ${idx + 1}: ${schema['@type'] || 'Unknown'}`);
const { issues, warnings } = validateSchema(schema);
if (issues.length === 0) {
log.success('No critical issues');
} else {
issues.forEach(issue => log.error(` ${issue}`));
}
if (warnings.length > 0) {
warnings.forEach(warning => log.warning(` ${warning}`));
}
});
}
// Validate Open Graph
console.log('\n Open Graph:');
const ogValidation = validateOpenGraph(html);
if (ogValidation.issues.length === 0) {
log.success('All required Open Graph tags present');
} else {
ogValidation.issues.forEach(issue => log.error(` ${issue}`));
}
if (ogValidation.warnings.length > 0) {
ogValidation.warnings.forEach(warning => log.warning(` ${warning}`));
}
// Validate Twitter Cards
console.log('\n Twitter Card:');
const twitterValidation = validateTwitterCard(html);
if (twitterValidation.issues.length === 0) {
log.success('All required Twitter Card tags present');
} else {
twitterValidation.issues.forEach(issue => log.error(` ${issue}`));
}
if (twitterValidation.warnings.length > 0) {
twitterValidation.warnings.forEach(warning => log.warning(` ${warning}`));
}
return {
schemas: schemas.length,
issues: ogValidation.issues.length + twitterValidation.issues.length,
warnings: ogValidation.warnings.length + twitterValidation.warnings.length,
};
} catch (error) {
log.error(`Failed to validate ${pageName}: ${error.message}`);
return { schemas: 0, issues: 1, warnings: 0 };
}
}
/**
* Main execution
*/
async function main() {
console.log('\n╔════════════════════════════════════════════════════════╗');
console.log('║ Schema Validation for WorkRoot IT Solutions ║');
console.log('╚════════════════════════════════════════════════════════╝\n');
const distDir = path.join(__dirname, '..', 'dist');
if (!fs.existsSync(distDir)) {
log.error('Build directory not found. Please run "npm run build" first.');
process.exit(1);
}
// Pages to validate
const pages = [
{ path: path.join(distDir, 'index.html'), name: 'Homepage' },
{ path: path.join(distDir, 'about', 'index.html'), name: 'About' },
{ path: path.join(distDir, 'services', 'index.html'), name: 'Services' },
{ path: path.join(distDir, 'contact', 'index.html'), name: 'Contact' },
{ path: path.join(distDir, 'blog', 'index.html'), name: 'Blog Index' },
];
// Find a blog post to validate
const blogDir = path.join(distDir, 'blog');
if (fs.existsSync(blogDir)) {
const blogPosts = fs.readdirSync(blogDir)
.filter(file => fs.statSync(path.join(blogDir, file)).isDirectory());
if (blogPosts.length > 0) {
pages.push({
path: path.join(blogDir, blogPosts[0], 'index.html'),
name: `Blog Post (${blogPosts[0]})`
});
}
}
let totalSchemas = 0;
let totalIssues = 0;
let totalWarnings = 0;
for (const page of pages) {
if (fs.existsSync(page.path)) {
const result = await validatePage(page.path, page.name);
totalSchemas += result.schemas;
totalIssues += result.issues;
totalWarnings += result.warnings;
} else {
log.warning(`Page not found: ${page.name}`);
}
}
// Summary
log.section('Validation Summary');
console.log(`Total schemas found: ${COLORS.cyan}${totalSchemas}${COLORS.reset}`);
console.log(`Total issues: ${totalIssues > 0 ? COLORS.red : COLORS.green}${totalIssues}${COLORS.reset}`);
console.log(`Total warnings: ${totalWarnings > 0 ? COLORS.yellow : COLORS.green}${totalWarnings}${COLORS.reset}`);
if (totalIssues === 0) {
log.success('\nAll validations passed! 🎉');
} else {
log.error('\nPlease fix the issues above before deployment.');
}
console.log('\n');
process.exit(totalIssues > 0 ? 1 : 0);
}
main();