- Remove next/script dependency causing SSR issues - Use regular script tag for server-side rendering - Add real SEO verification test that checks rendered output - All 7/7 SEO checks now passing
159 lines
5.2 KiB
JavaScript
159 lines
5.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* REAL SEO Verification Test
|
|
* Tests actual rendered HTML output, not just file existence
|
|
*/
|
|
|
|
const https = require('https');
|
|
const http = require('http');
|
|
|
|
const BASE_URL = 'localhost';
|
|
const PORT = 3000;
|
|
|
|
function fetchPage(path) {
|
|
return new Promise((resolve, reject) => {
|
|
const req = http.get({ hostname: BASE_URL, port: PORT, path }, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => resolve(data));
|
|
});
|
|
req.on('error', reject);
|
|
req.setTimeout(5000, () => {
|
|
req.destroy();
|
|
reject(new Error('Timeout'));
|
|
});
|
|
});
|
|
}
|
|
|
|
function extractMetaTags(html) {
|
|
const tags = {};
|
|
|
|
// Title
|
|
const titleMatch = html.match(/<title>([^<]*)<\/title>/);
|
|
if (titleMatch) tags.title = titleMatch[1];
|
|
|
|
// Meta description
|
|
const descMatch = html.match(/<meta[^>]*name="description"[^>]*content="([^"]*)"[^>]*>/);
|
|
if (descMatch) tags.description = descMatch[1];
|
|
|
|
// Meta keywords
|
|
const keywordsMatch = html.match(/<meta[^>]*name="keywords"[^>]*content="([^"]*)"[^>]*>/);
|
|
if (keywordsMatch) tags.keywords = keywordsMatch[1];
|
|
|
|
// Canonical
|
|
const canonicalMatch = html.match(/<link[^>]*rel="canonical"[^>]*href="([^"]*)"[^>]*>/);
|
|
if (canonicalMatch) tags.canonical = canonicalMatch[1];
|
|
|
|
// Robots
|
|
const robotsMatch = html.match(/<meta[^>]*name="robots"[^>]*content="([^"]*)"[^>]*>/);
|
|
if (robotsMatch) tags.robots = robotsMatch[1];
|
|
|
|
// OpenGraph tags
|
|
const ogTitle = html.match(/<meta[^>]*property="og:title"[^>]*content="([^"]*)"[^>]*>/);
|
|
if (ogTitle) tags.ogTitle = ogTitle[1];
|
|
|
|
const ogDesc = html.match(/<meta[^>]*property="og:description"[^>]*content="([^"]*)"[^>]*>/);
|
|
if (ogDesc) tags.ogDescription = ogDesc[1];
|
|
|
|
const ogUrl = html.match(/<meta[^>]*property="og:url"[^>]*content="([^"]*)"[^>]*>/);
|
|
if (ogUrl) tags.ogUrl = ogUrl[1];
|
|
|
|
// Twitter cards
|
|
const twitterCard = html.match(/<meta[^>]*name="twitter:card"[^>]*content="([^"]*)"[^>]*>/);
|
|
if (twitterCard) tags.twitterCard = twitterCard[1];
|
|
|
|
return tags;
|
|
}
|
|
|
|
function checkJsonLd(html) {
|
|
const schemas = [];
|
|
const scriptMatches = html.matchAll(/<script[^>]*type="application\/ld\+json"[^>]*>([\s\S]*?)<\/script>/g);
|
|
|
|
for (const match of scriptMatches) {
|
|
try {
|
|
const json = JSON.parse(match[1]);
|
|
schemas.push(json);
|
|
} catch (e) {
|
|
// Invalid JSON, skip
|
|
}
|
|
}
|
|
|
|
return schemas;
|
|
}
|
|
|
|
async function runTests() {
|
|
console.log('🔍 Testing ACTUAL Rendered SEO Output...\n');
|
|
console.log(`Testing: http://${BASE_URL}:${PORT}/sr\n`);
|
|
|
|
try {
|
|
const html = await fetchPage('/sr');
|
|
|
|
console.log('✅ Page fetched successfully');
|
|
console.log(` Size: ${(html.length / 1024).toFixed(1)} KB\n`);
|
|
|
|
// Test 1: Meta Tags
|
|
console.log('📋 META TAGS:');
|
|
const meta = extractMetaTags(html);
|
|
|
|
console.log(` Title: ${meta.title ? '✅ ' + meta.title.substring(0, 60) + '...' : '❌ MISSING'}`);
|
|
console.log(` Description: ${meta.description ? '✅ ' + meta.description.substring(0, 60) + '...' : '❌ MISSING'}`);
|
|
console.log(` Keywords: ${meta.keywords ? '✅ ' + meta.keywords.split(',').length + ' keywords' : '❌ MISSING'}`);
|
|
console.log(` Canonical: ${meta.canonical ? '✅ ' + meta.canonical : '❌ MISSING'}`);
|
|
console.log(` Robots: ${meta.robots ? '✅ ' + meta.robots : '❌ MISSING'}`);
|
|
console.log();
|
|
|
|
// Test 2: OpenGraph
|
|
console.log('📱 OPEN GRAPH:');
|
|
console.log(` og:title: ${meta.ogTitle ? '✅ Present' : '❌ MISSING'}`);
|
|
console.log(` og:description: ${meta.ogDescription ? '✅ Present' : '❌ MISSING'}`);
|
|
console.log(` og:url: ${meta.ogUrl ? '✅ ' + meta.ogUrl : '❌ MISSING'}`);
|
|
console.log();
|
|
|
|
// Test 3: Twitter Cards
|
|
console.log('🐦 TWITTER CARDS:');
|
|
console.log(` twitter:card: ${meta.twitterCard ? '✅ ' + meta.twitterCard : '❌ MISSING'}`);
|
|
console.log();
|
|
|
|
// Test 4: JSON-LD Schemas
|
|
console.log('🏗️ JSON-LD SCHEMAS:');
|
|
const schemas = checkJsonLd(html);
|
|
console.log(` Found: ${schemas.length} schema(s)`);
|
|
|
|
schemas.forEach((schema, i) => {
|
|
console.log(` Schema ${i + 1}: ✅ @type="${schema['@type']}"`);
|
|
});
|
|
console.log();
|
|
|
|
// Summary
|
|
const hasTitle = !!meta.title;
|
|
const hasDesc = !!meta.description;
|
|
const hasKeywords = !!meta.keywords;
|
|
const hasCanonical = !!meta.canonical;
|
|
const hasOg = !!meta.ogTitle;
|
|
const hasTwitter = !!meta.twitterCard;
|
|
const hasSchemas = schemas.length > 0;
|
|
|
|
const passed = [hasTitle, hasDesc, hasKeywords, hasCanonical, hasOg, hasTwitter, hasSchemas].filter(Boolean).length;
|
|
const total = 7;
|
|
|
|
console.log('='.repeat(50));
|
|
console.log(`Results: ${passed}/${total} checks passed`);
|
|
console.log('='.repeat(50));
|
|
|
|
if (passed === total) {
|
|
console.log('\n🎉 All SEO elements are rendering correctly!');
|
|
process.exit(0);
|
|
} else {
|
|
console.log(`\n⚠️ ${total - passed} SEO element(s) missing`);
|
|
process.exit(1);
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('\n❌ Error:', error.message);
|
|
console.log('\nMake sure the dev server is running on port 3000');
|
|
process.exit(1);
|
|
}
|
|
}
|
|
|
|
runTests();
|