Waseem771's picture
Create app.py
9826e36 verified
raw
history blame
24 kB
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;