Spaces:
Sleeping
Sleeping
| import React, { useState } from 'react'; | |
| import { Search, CheckCircle, XCircle, AlertCircle, Eye, Share2, Globe, Info } from 'lucide-react'; | |
| const SEOAnalyzer = () => { | |
| const [url, setUrl] = useState(''); | |
| const [loading, setLoading] = useState(false); | |
| const [results, setResults] = useState(null); | |
| const [activePreview, setActivePreview] = useState('google'); | |
| const analyzeSEO = async () => { | |
| if (!url) return; | |
| setLoading(true); | |
| setResults(null); | |
| // Ensure URL has protocol | |
| let targetUrl = url; | |
| if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) { | |
| targetUrl = 'https://' + targetUrl; | |
| } | |
| try { | |
| // Try to fetch using a more reliable CORS proxy | |
| const response = await fetch(`https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(targetUrl)}`, { | |
| method: 'GET', | |
| headers: { | |
| 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', | |
| } | |
| }); | |
| if (!response.ok) { | |
| throw new Error(`HTTP ${response.status}`); | |
| } | |
| const html = await response.text(); | |
| if (html && html.length > 100) { | |
| const parser = new DOMParser(); | |
| const doc = parser.parseFromString(html, 'text/html'); | |
| const analysis = extractMetaTags(doc, targetUrl); | |
| setResults(analysis); | |
| } else { | |
| throw new Error('Empty or invalid response'); | |
| } | |
| } catch (error) { | |
| console.error('Fetch failed:', error); | |
| // Show demo data with actual analysis | |
| const demoAnalysis = createDemoAnalysis(targetUrl); | |
| setResults(demoAnalysis); | |
| } | |
| setLoading(false); | |
| }; | |
| const createDemoAnalysis = (url) => { | |
| // Create realistic demo data that shows both good and bad examples | |
| const domain = url.replace(/(https?:\/\/)?(www\.)?/, '').split('/')[0]; | |
| return { | |
| basic: { | |
| title: `${domain.charAt(0).toUpperCase() + domain.slice(1)} - Home Page`, | |
| description: `Welcome to ${domain}. We provide excellent services and solutions for your needs. Visit us today!`, | |
| keywords: 'business, services, solutions, quality', | |
| robots: 'index, follow', | |
| canonical: url | |
| }, | |
| openGraph: { | |
| title: `${domain.charAt(0).toUpperCase() + domain.slice(1)} - Social Title`, | |
| description: `Discover amazing content and services at ${domain}. Join thousands of satisfied customers!`, | |
| image: `https://via.placeholder.com/1200x630/3B82F6/ffffff?text=${encodeURIComponent(domain)}`, | |
| url: url, | |
| type: 'website' | |
| }, | |
| twitter: { | |
| card: 'summary_large_image', | |
| title: `${domain} - Twitter Optimized`, | |
| description: `Follow ${domain} for the latest updates and exclusive content!`, | |
| image: `https://via.placeholder.com/1200x600/1DA1F2/ffffff?text=${encodeURIComponent(domain)}` | |
| }, | |
| structuredData: [ | |
| { | |
| "@context": "https://schema.org", | |
| "@type": "Organization", | |
| "name": domain, | |
| "url": url, | |
| "description": `Official website of ${domain}` | |
| } | |
| ], | |
| url, | |
| isDemo: true, | |
| issues: [ | |
| 'Title could be more descriptive', | |
| 'Meta description is too generic', | |
| 'Missing important Open Graph tags', | |
| 'No Twitter card optimization' | |
| ] | |
| }; | |
| }; | |
| const extractMetaTags = (doc, url) => { | |
| const getMetaContent = (name, property = false) => { | |
| const selector = property ? `meta[property="${name}"]` : `meta[name="${name}"]`; | |
| const meta = doc.querySelector(selector); | |
| return meta ? meta.getAttribute('content') : null; | |
| }; | |
| const title = doc.querySelector('title')?.textContent?.trim() || ''; | |
| const description = getMetaContent('description'); | |
| const keywords = getMetaContent('keywords'); | |
| const robots = getMetaContent('robots'); | |
| const canonical = doc.querySelector('link[rel="canonical"]')?.getAttribute('href') || ''; | |
| const viewport = getMetaContent('viewport'); | |
| // Open Graph tags | |
| const ogTitle = getMetaContent('og:title', true); | |
| const ogDescription = getMetaContent('og:description', true); | |
| const ogImage = getMetaContent('og:image', true); | |
| const ogUrl = getMetaContent('og:url', true); | |
| const ogType = getMetaContent('og:type', true); | |
| const ogSiteName = getMetaContent('og:site_name', true); | |
| // Twitter Card tags | |
| const twitterCard = getMetaContent('twitter:card'); | |
| const twitterTitle = getMetaContent('twitter:title'); | |
| const twitterDescription = getMetaContent('twitter:description'); | |
| const twitterImage = getMetaContent('twitter:image'); | |
| const twitterSite = getMetaContent('twitter:site'); | |
| // Structured data | |
| const jsonLd = Array.from(doc.querySelectorAll('script[type="application/ld+json"]')) | |
| .map(script => { | |
| try { | |
| return JSON.parse(script.textContent); | |
| } catch { | |
| return null; | |
| } | |
| }).filter(Boolean); | |
| // Generate SEO issues and recommendations | |
| const issues = []; | |
| if (!title || title.length < 30) issues.push('Title tag is too short (should be 30-60 characters)'); | |
| if (title && title.length > 60) issues.push('Title tag is too long (should be 30-60 characters)'); | |
| if (!description) issues.push('Missing meta description'); | |
| if (description && description.length < 120) issues.push('Meta description is too short (should be 120-160 characters)'); | |
| if (description && description.length > 160) issues.push('Meta description is too long (should be 120-160 characters)'); | |
| if (!ogTitle) issues.push('Missing Open Graph title'); | |
| if (!ogImage) issues.push('Missing Open Graph image'); | |
| if (!twitterCard) issues.push('Missing Twitter Card type'); | |
| return { | |
| basic: { | |
| title, | |
| description, | |
| keywords, | |
| robots, | |
| canonical, | |
| viewport | |
| }, | |
| openGraph: { | |
| title: ogTitle, | |
| description: ogDescription, | |
| image: ogImage, | |
| url: ogUrl, | |
| type: ogType, | |
| siteName: ogSiteName | |
| }, | |
| twitter: { | |
| card: twitterCard, | |
| title: twitterTitle, | |
| description: twitterDescription, | |
| image: twitterImage, | |
| site: twitterSite | |
| }, | |
| structuredData: jsonLd, | |
| url, | |
| issues, | |
| isDemo: false | |
| }; | |
| }; | |
| const getStatus = (value, minLength = 0, maxLength = 999) => { | |
| if (!value) return 'error'; | |
| if (value.length < minLength || value.length > maxLength) return 'warning'; | |
| return 'success'; | |
| }; | |
| const StatusIcon = ({ status }) => { | |
| if (status === 'success') return <CheckCircle className="w-5 h-5 text-green-500" />; | |
| if (status === 'error') return <XCircle className="w-5 h-5 text-red-500" />; | |
| return <AlertCircle className="w-5 h-5 text-yellow-500" />; | |
| }; | |
| const MetaTagRow = ({ label, value, status, recommendation, charCount }) => ( | |
| <div className="flex items-start gap-3 p-4 bg-gray-50 rounded-lg"> | |
| <StatusIcon status={status} /> | |
| <div className="flex-1"> | |
| <div className="flex justify-between items-start"> | |
| <h4 className="font-semibold text-gray-800">{label}</h4> | |
| {charCount && value && ( | |
| <span className={`text-xs px-2 py-1 rounded ${ | |
| status === 'success' ? 'bg-green-100 text-green-700' : | |
| status === 'warning' ? 'bg-yellow-100 text-yellow-700' : | |
| 'bg-red-100 text-red-700' | |
| }`}> | |
| {value.length} chars | |
| </span> | |
| )} | |
| </div> | |
| <p className="text-sm text-gray-600 mt-1 break-words"> | |
| {value || <span className="text-red-500">Not found</span>} | |
| </p> | |
| {recommendation && ( | |
| <p className="text-xs text-gray-500 mt-2">{recommendation}</p> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| const GooglePreview = ({ data }) => ( | |
| <div className="bg-white p-6 rounded-lg border shadow-sm"> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <Globe className="w-5 h-5 text-blue-600" /> | |
| Google Search Preview | |
| </h3> | |
| <div className="max-w-2xl"> | |
| <div className="text-sm text-green-700 mb-1">{data.url}</div> | |
| <div className="text-xl text-blue-600 hover:underline cursor-pointer mb-1 line-clamp-2"> | |
| {data.basic.title || 'Untitled Page'} | |
| </div> | |
| <div className="text-sm text-gray-700 leading-relaxed line-clamp-3"> | |
| {data.basic.description || 'No description available for this page.'} | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| const FacebookPreview = ({ data }) => { | |
| const title = data.openGraph.title || data.basic.title; | |
| const description = data.openGraph.description || data.basic.description; | |
| const image = data.openGraph.image; | |
| return ( | |
| <div className="bg-white p-6 rounded-lg border shadow-sm"> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <Share2 className="w-5 h-5 text-blue-600" /> | |
| Facebook Preview | |
| </h3> | |
| <div className="border rounded-lg overflow-hidden max-w-lg"> | |
| {image && ( | |
| <div className="h-64 bg-gray-200 flex items-center justify-center overflow-hidden"> | |
| <img | |
| src={image} | |
| alt="Preview" | |
| className="w-full h-full object-cover" | |
| onError={(e) => { | |
| e.target.src = 'https://via.placeholder.com/600x315/E5E7EB/6B7280?text=Image+Not+Found'; | |
| }} | |
| /> | |
| </div> | |
| )} | |
| <div className="p-4"> | |
| <div className="text-xs text-gray-500 uppercase mb-2">{new URL(data.url).hostname}</div> | |
| <div className="font-semibold text-gray-900 mb-2 line-clamp-2"> | |
| {title || 'Untitled'} | |
| </div> | |
| <div className="text-sm text-gray-600 line-clamp-2"> | |
| {description || 'No description available.'} | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| const TwitterPreview = ({ data }) => { | |
| const title = data.twitter.title || data.openGraph.title || data.basic.title; | |
| const description = data.twitter.description || data.openGraph.description || data.basic.description; | |
| const image = data.twitter.image || data.openGraph.image; | |
| return ( | |
| <div className="bg-white p-6 rounded-lg border shadow-sm"> | |
| <h3 className="text-lg font-semibold mb-4 flex items-center gap-2"> | |
| <Share2 className="w-5 h-5 text-blue-400" /> | |
| Twitter Preview | |
| </h3> | |
| <div className="border rounded-2xl overflow-hidden max-w-md bg-white"> | |
| {image && ( | |
| <div className="h-48 bg-gray-200 flex items-center justify-center overflow-hidden"> | |
| <img | |
| src={image} | |
| alt="Preview" | |
| className="w-full h-full object-cover" | |
| onError={(e) => { | |
| e.target.src = 'https://via.placeholder.com/600x300/E5E7EB/6B7280?text=Image+Not+Found'; | |
| }} | |
| /> | |
| </div> | |
| )} | |
| <div className="p-4"> | |
| <div className="font-semibold text-gray-900 mb-1 line-clamp-2"> | |
| {title || 'Untitled'} | |
| </div> | |
| <div className="text-sm text-gray-600 mb-2 line-clamp-2"> | |
| {description || 'No description available.'} | |
| </div> | |
| <div className="text-xs text-gray-500">{new URL(data.url).hostname}</div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| // Auto-run demo on component mount | |
| React.useEffect(() => { | |
| setUrl('https://example.com'); | |
| // Auto-analyze example.com for demonstration | |
| setTimeout(() => { | |
| const demoData = createDemoAnalysis('https://example.com'); | |
| setResults(demoData); | |
| }, 1000); | |
| }, []); | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 p-4"> | |
| <div className="max-w-6xl mx-auto"> | |
| <div className="text-center mb-8"> | |
| <h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent mb-4"> | |
| SEO Meta Tags Analyzer | |
| </h1> | |
| <p className="text-lg text-gray-600 max-w-2xl mx-auto"> | |
| Analyze and optimize your website's SEO meta tags with instant visual previews for Google, Facebook, and Twitter | |
| </p> | |
| </div> | |
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 mb-8 border border-white/20"> | |
| <div className="flex gap-4 items-center"> | |
| <input | |
| type="url" | |
| value={url} | |
| onChange={(e) => setUrl(e.target.value)} | |
| placeholder="Enter website URL (e.g., https://example.com)" | |
| className="flex-1 px-4 py-3 border border-gray-300 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent transition-all" | |
| /> | |
| <button | |
| onClick={analyzeSEO} | |
| disabled={loading || !url} | |
| className="px-6 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 text-white rounded-xl hover:from-blue-700 hover:to-indigo-700 disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-2 transition-all shadow-lg hover:shadow-xl" | |
| > | |
| {loading ? ( | |
| <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" /> | |
| ) : ( | |
| <Search className="w-5 h-5" /> | |
| )} | |
| {loading ? 'Analyzing...' : 'Analyze SEO'} | |
| </button> | |
| </div> | |
| </div> | |
| {results?.isDemo && ( | |
| <div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-4 mb-8"> | |
| <div className="flex items-start gap-3"> | |
| <Info className="w-5 h-5 text-blue-600 mt-0.5" /> | |
| <div> | |
| <p className="text-blue-800 font-medium"> | |
| Demo Analysis Active | |
| </p> | |
| <p className="text-sm text-blue-700 mt-1"> | |
| Due to browser security restrictions, this shows example data. In production, deploy server-side to analyze any website. | |
| </p> | |
| </div> | |
| </div> | |
| </div> | |
| )} | |
| {results && ( | |
| <div className="space-y-8"> | |
| {/* SEO Score Overview */} | |
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6">SEO Analysis Overview</h2> | |
| <div className="grid grid-cols-1 md:grid-cols-3 gap-6"> | |
| <div className="text-center p-4 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl"> | |
| <div className="text-3xl font-bold text-green-600"> | |
| {results.issues ? Math.max(0, 100 - (results.issues.length * 15)) : 85} | |
| </div> | |
| <div className="text-sm text-green-700 font-medium">SEO Score</div> | |
| </div> | |
| <div className="text-center p-4 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl"> | |
| <div className="text-3xl font-bold text-blue-600"> | |
| {results.structuredData?.length || 0} | |
| </div> | |
| <div className="text-sm text-blue-700 font-medium">Structured Data</div> | |
| </div> | |
| <div className="text-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl"> | |
| <div className="text-3xl font-bold text-purple-600"> | |
| {results.issues?.length || 0} | |
| </div> | |
| <div className="text-sm text-purple-700 font-medium">Issues Found</div> | |
| </div> | |
| </div> | |
| {results.issues && results.issues.length > 0 && ( | |
| <div className="mt-6 p-4 bg-yellow-50 rounded-xl"> | |
| <h3 className="font-semibold text-yellow-800 mb-2">Recommendations:</h3> | |
| <ul className="space-y-1"> | |
| {results.issues.map((issue, index) => ( | |
| <li key={index} className="text-sm text-yellow-700 flex items-center gap-2"> | |
| <AlertCircle className="w-4 h-4" /> | |
| {issue} | |
| </li> | |
| ))} | |
| </ul> | |
| </div> | |
| )} | |
| </div> | |
| {/* Basic SEO Tags */} | |
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6">Basic SEO Tags</h2> | |
| <div className="space-y-4"> | |
| <MetaTagRow | |
| label="Title Tag" | |
| value={results.basic.title} | |
| status={getStatus(results.basic.title, 30, 60)} | |
| recommendation="Should be 30-60 characters and include your main keyword." | |
| charCount={true} | |
| /> | |
| <MetaTagRow | |
| label="Meta Description" | |
| value={results.basic.description} | |
| status={getStatus(results.basic.description, 120, 160)} | |
| recommendation="Should be 120-160 characters and compelling to encourage clicks." | |
| charCount={true} | |
| /> | |
| <MetaTagRow | |
| label="Meta Keywords" | |
| value={results.basic.keywords} | |
| status={results.basic.keywords ? 'success' : 'warning'} | |
| recommendation="Optional - most search engines don't use this anymore." | |
| /> | |
| <MetaTagRow | |
| label="Robots Meta Tag" | |
| value={results.basic.robots} | |
| status={results.basic.robots ? 'success' : 'warning'} | |
| recommendation="Controls how search engines crawl and index your page." | |
| /> | |
| <MetaTagRow | |
| label="Canonical URL" | |
| value={results.basic.canonical} | |
| status={results.basic.canonical ? 'success' : 'warning'} | |
| recommendation="Helps prevent duplicate content issues." | |
| /> | |
| </div> | |
| </div> | |
| {/* Open Graph Tags */} | |
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6">Open Graph Tags (Facebook/LinkedIn)</h2> | |
| <div className="space-y-4"> | |
| <MetaTagRow | |
| label="OG Title" | |
| value={results.openGraph.title} | |
| status={getStatus(results.openGraph.title)} | |
| recommendation="Should be engaging and optimized for social sharing." | |
| /> | |
| <MetaTagRow | |
| label="OG Description" | |
| value={results.openGraph.description} | |
| status={getStatus(results.openGraph.description)} | |
| recommendation="Should be compelling and encourage social shares." | |
| /> | |
| <MetaTagRow | |
| label="OG Image" | |
| value={results.openGraph.image} | |
| status={getStatus(results.openGraph.image)} | |
| recommendation="Should be 1200x630px for optimal display on social media." | |
| /> | |
| <MetaTagRow | |
| label="OG Type" | |
| value={results.openGraph.type} | |
| status={results.openGraph.type ? 'success' : 'warning'} | |
| recommendation="Specify content type (website, article, etc.)." | |
| /> | |
| </div> | |
| </div> | |
| {/* Twitter Card Tags */} | |
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6">Twitter Card Tags</h2> | |
| <div className="space-y-4"> | |
| <MetaTagRow | |
| label="Twitter Card Type" | |
| value={results.twitter.card} | |
| status={getStatus(results.twitter.card)} | |
| recommendation="Use 'summary_large_image' for best visual impact." | |
| /> | |
| <MetaTagRow | |
| label="Twitter Title" | |
| value={results.twitter.title} | |
| status={results.twitter.title ? 'success' : 'warning'} | |
| recommendation="Optimized title for Twitter sharing." | |
| /> | |
| <MetaTagRow | |
| label="Twitter Description" | |
| value={results.twitter.description} | |
| status={results.twitter.description ? 'success' : 'warning'} | |
| recommendation="Engaging description for Twitter cards." | |
| /> | |
| <MetaTagRow | |
| label="Twitter Image" | |
| value={results.twitter.image} | |
| status={results.twitter.image ? 'success' : 'warning'} | |
| recommendation="Should be 1200x600px for summary_large_image cards." | |
| /> | |
| </div> | |
| </div> | |
| {/* Social Media Previews */} | |
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2"> | |
| <Eye className="w-6 h-6" /> | |
| Social Media Previews | |
| </h2> | |
| <div className="flex gap-2 mb-6 bg-gray-100 p-1 rounded-xl"> | |
| {[ | |
| { key: 'google', label: 'Google', icon: Globe }, | |
| { key: 'facebook', label: 'Facebook', icon: Share2 }, | |
| { key: 'twitter', label: 'Twitter', icon: Share2 } | |
| ].map(({ key, label, icon: Icon }) => ( | |
| <button | |
| key={key} | |
| onClick={() => setActivePreview(key)} | |
| className={`flex-1 px-4 py-2 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${ | |
| activePreview === key | |
| ? 'bg-white text-blue-600 shadow-sm' | |
| : 'text-gray-600 hover:text-gray-800' | |
| }`} | |
| > | |
| <Icon className="w-4 h-4" /> | |
| {label} | |
| </button> | |
| ))} | |
| </div> | |
| {activePreview === 'google' && <GooglePreview data={results} />} | |
| {activePreview === 'facebook' && <FacebookPreview data={results} />} | |
| {activePreview === 'twitter' && <TwitterPreview data={results} />} | |
| </div> | |
| {/* Structured Data */} | |
| {results.structuredData && results.structuredData.length > 0 && ( | |
| <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20"> | |
| <h2 className="text-2xl font-bold text-gray-800 mb-6">Structured Data (JSON-LD)</h2> | |
| <div className="bg-gray-50 rounded-xl p-4"> | |
| <p className="text-green-700 font-medium mb-4 flex items-center gap-2"> | |
| <CheckCircle className="w-5 h-5" /> | |
| Found {results.structuredData.length} structured data object(s) | |
| </p> | |
| <pre className="text-sm text-gray-700 overflow-x-auto whitespace-pre-wrap"> | |
| {JSON.stringify(results.structuredData, null, 2)} | |
| </pre> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| )} | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default SEOAnalyzer; |