First Init
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
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
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
#!/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();
|
||||
Reference in New Issue
Block a user