fix: JSON-LD schema rendering in SSR

- 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
This commit is contained in:
Unchained
2026-03-30 11:55:21 +02:00
parent 3accf4c244
commit ba4da3287d
2 changed files with 162 additions and 6 deletions

158
scripts/test-seo-real.js Normal file
View File

@@ -0,0 +1,158 @@
#!/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();

View File

@@ -1,4 +1,3 @@
import Script from 'next/script';
import { SchemaType } from '@/lib/seo/schema/types'; import { SchemaType } from '@/lib/seo/schema/types';
interface JsonLdProps { interface JsonLdProps {
@@ -6,11 +5,11 @@ interface JsonLdProps {
} }
/** /**
* React component to render JSON-LD schema markup * Server-safe JSON-LD schema component
* Uses Next.js Script component for proper loading * Renders directly to HTML for SSR (no client-side JS needed)
* *
* @param data - Single schema object or array of schemas * @param data - Single schema object or array of schemas
* @returns Script component with JSON-LD * @returns Script tag with JSON-LD
* @example * @example
* <JsonLd data={productSchema} /> * <JsonLd data={productSchema} />
* <JsonLd data={[productSchema, breadcrumbSchema]} /> * <JsonLd data={[productSchema, breadcrumbSchema]} />
@@ -22,11 +21,10 @@ export function JsonLd({ data }: JsonLdProps) {
return ( return (
<> <>
{schemas.map((schema, index) => ( {schemas.map((schema, index) => (
<Script <script
key={index} key={index}
id={`json-ld-${index}`} id={`json-ld-${index}`}
type="application/ld+json" type="application/ld+json"
strategy="afterInteractive"
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: JSON.stringify(schema), __html: JSON.stringify(schema),
}} }}