A simple SEO inspecter Tool, to get social media card previews

Add design customization features

+322 -68
+271 -56
src/components/SEOTester.tsx
··· 1 - import React, { useState } from 'react'; 1 + import React, { useState, useEffect } from 'react'; 2 2 import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 3 3 import { Button } from '@/components/ui/button'; 4 4 import { Input } from '@/components/ui/input'; 5 5 import { Badge } from '@/components/ui/badge'; 6 6 import { Separator } from '@/components/ui/separator'; 7 + import { Progress } from '@/components/ui/progress'; 7 8 import { useToast } from '@/hooks/use-toast'; 8 - import { Search, ExternalLink, Image, FileText, Tag, Globe, AlertCircle, CheckCircle } from 'lucide-react'; 9 + import { Search, ExternalLink, Image, FileText, Tag, Globe, AlertCircle, CheckCircle, Zap, TrendingUp, Eye, Share2, Target } from 'lucide-react'; 9 10 10 11 interface SEOData { 11 12 title?: string; ··· 21 22 canonical?: string; 22 23 keywords?: string; 23 24 url: string; 25 + h1?: string; 26 + metaRobots?: string; 27 + lang?: string; 28 + viewport?: string; 29 + charset?: string; 30 + } 31 + 32 + interface SEOScore { 33 + total: number; 34 + breakdown: { 35 + basic: number; 36 + social: number; 37 + technical: number; 38 + }; 24 39 } 25 40 26 41 const SEOTester = () => { ··· 28 43 const [isLoading, setIsLoading] = useState(false); 29 44 const [seoData, setSeoData] = useState<SEOData | null>(null); 30 45 const [error, setError] = useState<string | null>(null); 46 + const [progress, setProgress] = useState(0); 47 + const [seoScore, setSeoScore] = useState<SEOScore | null>(null); 48 + const [showResults, setShowResults] = useState(false); 31 49 const { toast } = useToast(); 32 50 33 51 const extractMetaData = (html: string, url: string): SEOData => { ··· 46 64 47 65 const title = doc.querySelector('title')?.textContent || ''; 48 66 const canonical = doc.querySelector('link[rel="canonical"]')?.getAttribute('href') || ''; 67 + const h1 = doc.querySelector('h1')?.textContent || ''; 68 + const htmlElement = doc.querySelector('html'); 69 + const lang = htmlElement?.getAttribute('lang') || ''; 49 70 50 71 return { 51 72 title, ··· 60 81 twitterImage: getMetaContent('twitter:image'), 61 82 canonical, 62 83 keywords: getMetaContent('keywords'), 84 + h1, 85 + metaRobots: getMetaContent('robots'), 86 + lang, 87 + viewport: getMetaContent('viewport'), 88 + charset: doc.querySelector('meta[charset]')?.getAttribute('charset') || '', 63 89 url 64 90 }; 65 91 }; 66 92 93 + const calculateSEOScore = (data: SEOData): SEOScore => { 94 + let basicScore = 0; 95 + let socialScore = 0; 96 + let technicalScore = 0; 97 + 98 + // Basic SEO (40 points max) 99 + if (data.title) basicScore += 15; 100 + if (data.description) basicScore += 15; 101 + if (data.h1) basicScore += 10; 102 + 103 + // Social Media (30 points max) 104 + if (data.ogTitle || data.title) socialScore += 8; 105 + if (data.ogDescription || data.description) socialScore += 8; 106 + if (data.ogImage) socialScore += 14; 107 + 108 + // Technical SEO (30 points max) 109 + if (data.canonical) technicalScore += 10; 110 + if (data.lang) technicalScore += 5; 111 + if (data.viewport) technicalScore += 5; 112 + if (data.charset) technicalScore += 5; 113 + if (!data.metaRobots || !data.metaRobots.includes('noindex')) technicalScore += 5; 114 + 115 + const total = basicScore + socialScore + technicalScore; 116 + 117 + return { 118 + total, 119 + breakdown: { 120 + basic: basicScore, 121 + social: socialScore, 122 + technical: technicalScore 123 + } 124 + }; 125 + }; 126 + 67 127 const analyzeSEO = async () => { 68 128 if (!url) { 69 129 toast({ ··· 79 139 setSeoData(null); 80 140 81 141 try { 142 + // Simulate progress updates 143 + setProgress(20); 144 + 82 145 // First, let's try to validate the URL format 83 146 const urlObj = new URL(url); 147 + setProgress(40); 84 148 85 149 // For demonstration, we'll use a CORS proxy service 86 150 // In a real app, you'd want to use a backend service 87 151 const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`; 88 152 const response = await fetch(proxyUrl); 153 + setProgress(70); 89 154 90 155 if (!response.ok) { 91 156 throw new Error(`HTTP error! status: ${response.status}`); ··· 97 162 throw new Error('No content received from the website'); 98 163 } 99 164 165 + setProgress(90); 100 166 const extractedData = extractMetaData(data.contents, url); 167 + const score = calculateSEOScore(extractedData); 168 + 101 169 setSeoData(extractedData); 170 + setSeoScore(score); 171 + setProgress(100); 172 + 173 + // Trigger results animation 174 + setTimeout(() => { 175 + setShowResults(true); 176 + }, 200); 102 177 103 178 toast({ 104 179 title: "Success", ··· 124 199 }); 125 200 } finally { 126 201 setIsLoading(false); 202 + if (!error) { 203 + setTimeout(() => setProgress(0), 1000); 204 + } 127 205 } 128 206 }; 129 207 ··· 132 210 analyzeSEO(); 133 211 }; 134 212 135 - const getScoreColor = (hasValue: boolean) => { 136 - return hasValue ? 'success' : 'destructive'; 213 + const getScoreColor = (score: number) => { 214 + if (score >= 80) return 'success'; 215 + if (score >= 60) return 'warning'; 216 + return 'destructive'; 217 + }; 218 + 219 + const getScoreGrade = (score: number) => { 220 + if (score >= 90) return 'A+'; 221 + if (score >= 80) return 'A'; 222 + if (score >= 70) return 'B'; 223 + if (score >= 60) return 'C'; 224 + if (score >= 50) return 'D'; 225 + return 'F'; 137 226 }; 138 227 139 228 const ScoreIndicator = ({ hasValue, label }: { hasValue: boolean; label: string }) => ( 140 - <div className="flex items-center gap-2"> 141 - {hasValue ? ( 142 - <CheckCircle className="h-4 w-4 text-success" /> 143 - ) : ( 144 - <AlertCircle className="h-4 w-4 text-destructive" /> 145 - )} 146 - <span className="text-sm">{label}</span> 229 + <div className="flex items-center gap-2 group cursor-default"> 230 + <div className="relative"> 231 + {hasValue ? ( 232 + <CheckCircle className="h-4 w-4 text-success group-hover:scale-110 transition-transform duration-200" /> 233 + ) : ( 234 + <AlertCircle className="h-4 w-4 text-destructive group-hover:scale-110 transition-transform duration-200" /> 235 + )} 236 + </div> 237 + <span className="text-sm group-hover:text-foreground transition-colors duration-200">{label}</span> 147 238 </div> 148 239 ); 149 240 241 + const AnimatedCounter = ({ value, duration = 1000 }: { value: number; duration?: number }) => { 242 + const [count, setCount] = useState(0); 243 + 244 + useEffect(() => { 245 + if (showResults) { 246 + let start = 0; 247 + const increment = value / (duration / 16); 248 + const timer = setInterval(() => { 249 + start += increment; 250 + if (start >= value) { 251 + setCount(value); 252 + clearInterval(timer); 253 + } else { 254 + setCount(Math.floor(start)); 255 + } 256 + }, 16); 257 + return () => clearInterval(timer); 258 + } 259 + }, [value, duration, showResults]); 260 + 261 + return <span>{count}</span>; 262 + }; 263 + 150 264 return ( 151 - <div className="min-h-screen bg-gradient-subtle"> 152 - <div className="container mx-auto px-4 py-8"> 265 + <div className="min-h-screen bg-gradient-subtle relative overflow-hidden"> 266 + {/* Animated background elements */} 267 + <div className="absolute inset-0 overflow-hidden pointer-events-none"> 268 + <div className="absolute -top-40 -right-40 w-80 h-80 bg-primary/5 rounded-full animate-pulse-glow"></div> 269 + <div className="absolute -bottom-40 -left-40 w-96 h-96 bg-primary-glow/5 rounded-full animate-pulse-glow" style={{animationDelay: '1s'}}></div> 270 + </div> 271 + 272 + <div className="container mx-auto px-4 py-8 relative z-10"> 153 273 <div className="max-w-4xl mx-auto"> 154 274 {/* Header */} 155 - <div className="text-center mb-8"> 156 - <h1 className="text-4xl font-bold text-foreground mb-4">SEO Analyzer</h1> 157 - <p className="text-lg text-muted-foreground"> 158 - Analyze any website's SEO metadata and social media tags 275 + <div className="text-center mb-8 animate-fade-in"> 276 + <div className="inline-flex items-center gap-2 mb-4"> 277 + <div className="p-2 bg-gradient-primary rounded-xl shadow-elegant"> 278 + <Target className="h-6 w-6 text-white" /> 279 + </div> 280 + <h1 className="text-4xl font-bold text-foreground">SEO Analyzer</h1> 281 + </div> 282 + <p className="text-lg text-muted-foreground max-w-2xl mx-auto"> 283 + Get comprehensive SEO insights with our advanced analyzer. 284 + Discover optimization opportunities and track your website's performance. 159 285 </p> 160 286 </div> 161 287 162 288 {/* URL Input Form */} 163 - <Card className="mb-8 shadow-card"> 289 + <Card className="mb-8 shadow-card animate-scale-in border-0 bg-card/80 backdrop-blur-sm"> 164 290 <CardContent className="pt-6"> 165 - <form onSubmit={handleSubmit} className="flex gap-4"> 166 - <div className="flex-1"> 167 - <Input 168 - type="url" 169 - placeholder="https://example.com" 170 - value={url} 171 - onChange={(e) => setUrl(e.target.value)} 172 - className="text-lg" 173 - required 174 - /> 291 + <form onSubmit={handleSubmit} className="space-y-4"> 292 + <div className="flex gap-4"> 293 + <div className="flex-1 relative"> 294 + <Input 295 + type="url" 296 + placeholder="https://example.com" 297 + value={url} 298 + onChange={(e) => setUrl(e.target.value)} 299 + className="text-lg pr-12 border-2 focus:border-primary transition-all duration-300" 300 + required 301 + /> 302 + <Globe className="absolute right-3 top-1/2 transform -translate-y-1/2 h-5 w-5 text-muted-foreground" /> 303 + </div> 304 + <Button 305 + type="submit" 306 + size="lg" 307 + disabled={isLoading} 308 + className="bg-gradient-primary hover:shadow-elegant transition-all duration-300 hover:scale-105 relative overflow-hidden group" 309 + > 310 + <div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent translate-x-[-100%] group-hover:translate-x-[100%] transition-transform duration-700 ease-in-out"></div> 311 + {isLoading ? ( 312 + <> 313 + <Zap className="h-4 w-4 mr-2 animate-bounce-subtle" /> 314 + Analyzing... 315 + </> 316 + ) : ( 317 + <> 318 + <Search className="h-4 w-4 mr-2" /> 319 + Analyze SEO 320 + </> 321 + )} 322 + </Button> 175 323 </div> 176 - <Button 177 - type="submit" 178 - size="lg" 179 - disabled={isLoading} 180 - className="bg-gradient-primary hover:shadow-elegant transition-all duration-300" 181 - > 182 - <Search className="h-4 w-4 mr-2" /> 183 - {isLoading ? 'Analyzing...' : 'Analyze'} 184 - </Button> 324 + 325 + {isLoading && ( 326 + <div className="space-y-2 animate-fade-in"> 327 + <div className="flex items-center justify-between text-sm"> 328 + <span className="text-muted-foreground">Analyzing website...</span> 329 + <span className="text-primary font-medium">{progress}%</span> 330 + </div> 331 + <Progress value={progress} className="h-2" /> 332 + </div> 333 + )} 185 334 </form> 186 335 </CardContent> 187 336 </Card> ··· 199 348 )} 200 349 201 350 {/* Results */} 202 - {seoData && ( 203 - <div className="space-y-6"> 204 - {/* SEO Score Overview */} 205 - <Card className="shadow-card"> 351 + {seoData && seoScore && ( 352 + <div className={`space-y-6 ${showResults ? 'animate-fade-in-up' : 'opacity-0'}`}> 353 + {/* SEO Score Dashboard */} 354 + <Card className="shadow-card border-0 bg-gradient-primary text-white relative overflow-hidden"> 355 + <div className="absolute inset-0 bg-gradient-to-br from-primary via-primary-glow to-primary opacity-90"></div> 356 + <CardContent className="pt-6 relative z-10"> 357 + <div className="text-center mb-6"> 358 + <div className="inline-flex items-center gap-3 mb-4"> 359 + <TrendingUp className="h-8 w-8" /> 360 + <h2 className="text-2xl font-bold">SEO Score</h2> 361 + </div> 362 + <div className="relative inline-flex items-center justify-center"> 363 + <div className="text-6xl font-bold mb-2"> 364 + <AnimatedCounter value={seoScore.total} /> 365 + </div> 366 + <div className="absolute -top-2 -right-8 text-lg font-medium bg-white/20 px-2 py-1 rounded-full"> 367 + {getScoreGrade(seoScore.total)} 368 + </div> 369 + </div> 370 + <p className="text-white/80 mb-6">out of 100 points</p> 371 + </div> 372 + 373 + <div className="grid grid-cols-3 gap-4 text-center"> 374 + <div className="bg-white/10 rounded-lg p-4"> 375 + <div className="text-2xl font-bold mb-1"> 376 + <AnimatedCounter value={seoScore.breakdown.basic} /> 377 + </div> 378 + <div className="text-sm text-white/80">Basic SEO</div> 379 + <div className="text-xs text-white/60">/ 40 pts</div> 380 + </div> 381 + <div className="bg-white/10 rounded-lg p-4"> 382 + <div className="text-2xl font-bold mb-1"> 383 + <AnimatedCounter value={seoScore.breakdown.social} /> 384 + </div> 385 + <div className="text-sm text-white/80">Social Media</div> 386 + <div className="text-xs text-white/60">/ 30 pts</div> 387 + </div> 388 + <div className="bg-white/10 rounded-lg p-4"> 389 + <div className="text-2xl font-bold mb-1"> 390 + <AnimatedCounter value={seoScore.breakdown.technical} /> 391 + </div> 392 + <div className="text-sm text-white/80">Technical</div> 393 + <div className="text-xs text-white/60">/ 30 pts</div> 394 + </div> 395 + </div> 396 + </CardContent> 397 + </Card> 398 + 399 + {/* Quick Overview */} 400 + <Card className="shadow-card border-0 bg-card/80 backdrop-blur-sm"> 206 401 <CardHeader> 207 402 <CardTitle className="flex items-center gap-2"> 208 - <Tag className="h-5 w-5" /> 209 - SEO Overview 403 + <Eye className="h-5 w-5" /> 404 + Quick Overview 210 405 </CardTitle> 211 406 </CardHeader> 212 407 <CardContent> ··· 215 410 <ScoreIndicator hasValue={!!seoData.description} label="Meta Description" /> 216 411 <ScoreIndicator hasValue={!!seoData.ogImage} label="OG Image" /> 217 412 <ScoreIndicator hasValue={!!seoData.canonical} label="Canonical URL" /> 413 + <ScoreIndicator hasValue={!!seoData.h1} label="H1 Tag" /> 414 + <ScoreIndicator hasValue={!!seoData.lang} label="Language" /> 415 + <ScoreIndicator hasValue={!!seoData.viewport} label="Viewport" /> 416 + <ScoreIndicator hasValue={!!seoData.charset} label="Charset" /> 218 417 </div> 219 418 </CardContent> 220 419 </Card> 221 420 222 421 {/* Basic SEO */} 223 - <Card className="shadow-card"> 422 + <Card className="shadow-card border-0 bg-card/80 backdrop-blur-sm"> 224 423 <CardHeader> 225 424 <CardTitle className="flex items-center gap-2"> 226 - <FileText className="h-5 w-5" /> 425 + <FileText className="h-5 w-5 text-primary" /> 227 426 Basic SEO 427 + <Badge variant="secondary" className="ml-auto"> 428 + {seoScore.breakdown.basic}/40 pts 429 + </Badge> 228 430 </CardTitle> 229 431 </CardHeader> 230 - <CardContent className="space-y-4"> 231 - <div> 232 - <label className="text-sm font-medium text-muted-foreground">Page Title</label> 233 - <p className="mt-1 text-foreground">{seoData.title || 'Not found'}</p> 432 + <CardContent className="space-y-6"> 433 + <div className="group hover:bg-muted/30 p-3 rounded-lg transition-colors duration-200"> 434 + <label className="text-sm font-medium text-muted-foreground flex items-center gap-2"> 435 + <Tag className="h-3 w-3" /> 436 + Page Title 437 + {seoData.title && ( 438 + <Badge variant={seoData.title.length >= 30 && seoData.title.length <= 60 ? 'default' : 'secondary'} className="text-xs"> 439 + {seoData.title.length} chars 440 + </Badge> 441 + )} 442 + </label> 443 + <p className="mt-2 text-foreground font-medium">{seoData.title || 'Not found'}</p> 234 444 {seoData.title && ( 235 - <p className="text-xs text-muted-foreground mt-1"> 236 - Length: {seoData.title.length} characters 237 - </p> 445 + <div className="mt-2 text-xs text-muted-foreground"> 446 + {seoData.title.length < 30 && <span className="text-yellow-600">⚠️ Too short (recommended: 30-60 characters)</span>} 447 + {seoData.title.length > 60 && <span className="text-yellow-600">⚠️ Too long (recommended: 30-60 characters)</span>} 448 + {seoData.title.length >= 30 && seoData.title.length <= 60 && <span className="text-green-600">✓ Good length</span>} 449 + </div> 238 450 )} 239 451 </div> 240 452 ··· 280 492 </Card> 281 493 282 494 {/* Open Graph */} 283 - <Card className="shadow-card"> 495 + <Card className="shadow-card border-0 bg-card/80 backdrop-blur-sm"> 284 496 <CardHeader> 285 497 <CardTitle className="flex items-center gap-2"> 286 - <Globe className="h-5 w-5" /> 498 + <Share2 className="h-5 w-5 text-primary" /> 287 499 Open Graph (Facebook) 500 + <Badge variant="secondary" className="ml-auto"> 501 + {seoScore.breakdown.social}/30 pts 502 + </Badge> 288 503 </CardTitle> 289 504 </CardHeader> 290 505 <CardContent className="space-y-4"> ··· 325 540 </Card> 326 541 327 542 {/* Twitter Cards */} 328 - <Card className="shadow-card"> 543 + <Card className="shadow-card border-0 bg-card/80 backdrop-blur-sm"> 329 544 <CardHeader> 330 545 <CardTitle className="flex items-center gap-2"> 331 - <Image className="h-5 w-5" /> 546 + <Image className="h-5 w-5 text-primary" /> 332 547 Twitter Cards 333 548 </CardTitle> 334 549 </CardHeader>
+12
src/index.css
··· 47 47 /* Shadows */ 48 48 --shadow-elegant: 0 10px 30px -10px hsl(var(--primary) / 0.2); 49 49 --shadow-card: 0 4px 12px -2px hsl(215 25% 27% / 0.08); 50 + --shadow-glow: 0 0 40px hsl(var(--primary) / 0.3); 51 + 52 + /* Animations */ 53 + --transition-smooth: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 54 + --transition-bounce: all 0.2s cubic-bezier(0.68, -0.55, 0.265, 1.55); 55 + 56 + --shimmer-bg: linear-gradient( 57 + 90deg, 58 + transparent, 59 + hsl(var(--primary) / 0.1), 60 + transparent 61 + ); 50 62 51 63 --radius: 0.5rem; 52 64
+39 -12
tailwind.config.ts
··· 78 78 }, 79 79 keyframes: { 80 80 "accordion-down": { 81 - from: { 82 - height: "0", 83 - }, 84 - to: { 85 - height: "var(--radix-accordion-content-height)", 86 - }, 81 + from: { height: "0", opacity: "0" }, 82 + to: { height: "var(--radix-accordion-content-height)", opacity: "1" } 87 83 }, 88 84 "accordion-up": { 89 - from: { 90 - height: "var(--radix-accordion-content-height)", 91 - }, 92 - to: { 93 - height: "0", 94 - }, 85 + from: { height: "var(--radix-accordion-content-height)", opacity: "1" }, 86 + to: { height: "0", opacity: "0" } 87 + }, 88 + "fade-in": { 89 + "0%": { opacity: "0", transform: "translateY(10px)" }, 90 + "100%": { opacity: "1", transform: "translateY(0)" } 91 + }, 92 + "fade-in-up": { 93 + "0%": { opacity: "0", transform: "translateY(20px)" }, 94 + "100%": { opacity: "1", transform: "translateY(0)" } 95 + }, 96 + "scale-in": { 97 + "0%": { transform: "scale(0.95)", opacity: "0" }, 98 + "100%": { transform: "scale(1)", opacity: "1" } 99 + }, 100 + "slide-in-right": { 101 + "0%": { transform: "translateX(100%)", opacity: "0" }, 102 + "100%": { transform: "translateX(0)", opacity: "1" } 103 + }, 104 + "pulse-glow": { 105 + "0%, 100%": { boxShadow: "0 0 20px hsl(var(--primary) / 0.3)" }, 106 + "50%": { boxShadow: "0 0 40px hsl(var(--primary) / 0.6)" } 107 + }, 108 + "bounce-subtle": { 109 + "0%, 100%": { transform: "translateY(0)" }, 110 + "50%": { transform: "translateY(-2px)" } 95 111 }, 112 + "shimmer": { 113 + "0%": { backgroundPosition: "-200% 0" }, 114 + "100%": { backgroundPosition: "200% 0" } 115 + } 96 116 }, 97 117 animation: { 98 118 "accordion-down": "accordion-down 0.2s ease-out", 99 119 "accordion-up": "accordion-up 0.2s ease-out", 120 + "fade-in": "fade-in 0.3s ease-out", 121 + "fade-in-up": "fade-in-up 0.4s ease-out", 122 + "scale-in": "scale-in 0.2s ease-out", 123 + "slide-in-right": "slide-in-right 0.3s ease-out", 124 + "pulse-glow": "pulse-glow 2s infinite", 125 + "bounce-subtle": "bounce-subtle 2s infinite", 126 + "shimmer": "shimmer 2s infinite linear" 100 127 }, 101 128 }, 102 129 },