From ba4da3287df0fac1ecff677764c974790c89ed85 Mon Sep 17 00:00:00 2001 From: Unchained Date: Mon, 30 Mar 2026 11:55:21 +0200 Subject: [PATCH] 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 --- scripts/test-seo-real.js | 158 ++++++++++++++++++++++++++++++++++ src/components/seo/JsonLd.tsx | 10 +-- 2 files changed, 162 insertions(+), 6 deletions(-) create mode 100644 scripts/test-seo-real.js diff --git a/scripts/test-seo-real.js b/scripts/test-seo-real.js new file mode 100644 index 0000000..ba5e13e --- /dev/null +++ b/scripts/test-seo-real.js @@ -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>/); + 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(); diff --git a/src/components/seo/JsonLd.tsx b/src/components/seo/JsonLd.tsx index 4af447c..80bd31e 100644 --- a/src/components/seo/JsonLd.tsx +++ b/src/components/seo/JsonLd.tsx @@ -1,4 +1,3 @@ -import Script from 'next/script'; import { SchemaType } from '@/lib/seo/schema/types'; interface JsonLdProps { @@ -6,11 +5,11 @@ interface JsonLdProps { } /** - * React component to render JSON-LD schema markup - * Uses Next.js Script component for proper loading + * Server-safe JSON-LD schema component + * Renders directly to HTML for SSR (no client-side JS needed) * * @param data - Single schema object or array of schemas - * @returns Script component with JSON-LD + * @returns Script tag with JSON-LD * @example * <JsonLd data={productSchema} /> * <JsonLd data={[productSchema, breadcrumbSchema]} /> @@ -22,11 +21,10 @@ export function JsonLd({ data }: JsonLdProps) { return ( <> {schemas.map((schema, index) => ( - <Script + <script key={index} id={`json-ld-${index}`} type="application/ld+json" - strategy="afterInteractive" dangerouslySetInnerHTML={{ __html: JSON.stringify(schema), }}