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

Configure Feed

Select the types of activity you want to include in your feed.

Add SEO tester page

+459 -50
+5 -5
index.html
··· 3 3 <head> 4 4 <meta charset="UTF-8" /> 5 5 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 6 - <title>og-inspector-buddy</title> 7 - <meta name="description" content="Lovable Generated Project" /> 8 - <meta name="author" content="Lovable" /> 6 + <title>SEO Analyzer - Free SEO Testing Tool</title> 7 + <meta name="description" content="Free SEO analyzer tool to test website metadata, Open Graph tags, Twitter cards, and more. Analyze any website's SEO performance instantly." /> 8 + <meta name="author" content="SEO Analyzer" /> 9 9 10 - <meta property="og:title" content="og-inspector-buddy" /> 11 - <meta property="og:description" content="Lovable Generated Project" /> 10 + <meta property="og:title" content="SEO Analyzer - Free SEO Testing Tool" /> 11 + <meta property="og:description" content="Free SEO analyzer tool to test website metadata, Open Graph tags, Twitter cards, and more. Analyze any website's SEO performance instantly." /> 12 12 <meta property="og:type" content="website" /> 13 13 <meta property="og:image" content="https://lovable.dev/opengraph-image-p98pqg.png" /> 14 14
+379
src/components/SEOTester.tsx
··· 1 + import React, { useState } from 'react'; 2 + import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; 3 + import { Button } from '@/components/ui/button'; 4 + import { Input } from '@/components/ui/input'; 5 + import { Badge } from '@/components/ui/badge'; 6 + import { Separator } from '@/components/ui/separator'; 7 + import { useToast } from '@/hooks/use-toast'; 8 + import { Search, ExternalLink, Image, FileText, Tag, Globe, AlertCircle, CheckCircle } from 'lucide-react'; 9 + 10 + interface SEOData { 11 + title?: string; 12 + description?: string; 13 + ogTitle?: string; 14 + ogDescription?: string; 15 + ogImage?: string; 16 + ogType?: string; 17 + twitterCard?: string; 18 + twitterTitle?: string; 19 + twitterDescription?: string; 20 + twitterImage?: string; 21 + canonical?: string; 22 + keywords?: string; 23 + url: string; 24 + } 25 + 26 + const SEOTester = () => { 27 + const [url, setUrl] = useState(''); 28 + const [isLoading, setIsLoading] = useState(false); 29 + const [seoData, setSeoData] = useState<SEOData | null>(null); 30 + const [error, setError] = useState<string | null>(null); 31 + const { toast } = useToast(); 32 + 33 + const extractMetaData = (html: string, url: string): SEOData => { 34 + const parser = new DOMParser(); 35 + const doc = parser.parseFromString(html, 'text/html'); 36 + 37 + const getMetaContent = (name: string, property?: string) => { 38 + if (property) { 39 + const element = doc.querySelector(`meta[property="${property}"]`); 40 + return element?.getAttribute('content') || ''; 41 + } 42 + const element = doc.querySelector(`meta[name="${name}"]`) || 43 + doc.querySelector(`meta[property="${name}"]`); 44 + return element?.getAttribute('content') || ''; 45 + }; 46 + 47 + const title = doc.querySelector('title')?.textContent || ''; 48 + const canonical = doc.querySelector('link[rel="canonical"]')?.getAttribute('href') || ''; 49 + 50 + return { 51 + title, 52 + description: getMetaContent('description'), 53 + ogTitle: getMetaContent('', 'og:title'), 54 + ogDescription: getMetaContent('', 'og:description'), 55 + ogImage: getMetaContent('', 'og:image'), 56 + ogType: getMetaContent('', 'og:type'), 57 + twitterCard: getMetaContent('twitter:card'), 58 + twitterTitle: getMetaContent('twitter:title'), 59 + twitterDescription: getMetaContent('twitter:description'), 60 + twitterImage: getMetaContent('twitter:image'), 61 + canonical, 62 + keywords: getMetaContent('keywords'), 63 + url 64 + }; 65 + }; 66 + 67 + const analyzeSEO = async () => { 68 + if (!url) { 69 + toast({ 70 + title: "Error", 71 + description: "Please enter a valid URL", 72 + variant: "destructive", 73 + }); 74 + return; 75 + } 76 + 77 + setIsLoading(true); 78 + setError(null); 79 + setSeoData(null); 80 + 81 + try { 82 + // First, let's try to validate the URL format 83 + const urlObj = new URL(url); 84 + 85 + // For demonstration, we'll use a CORS proxy service 86 + // In a real app, you'd want to use a backend service 87 + const proxyUrl = `https://api.allorigins.win/get?url=${encodeURIComponent(url)}`; 88 + const response = await fetch(proxyUrl); 89 + 90 + if (!response.ok) { 91 + throw new Error(`HTTP error! status: ${response.status}`); 92 + } 93 + 94 + const data = await response.json(); 95 + 96 + if (!data.contents) { 97 + throw new Error('No content received from the website'); 98 + } 99 + 100 + const extractedData = extractMetaData(data.contents, url); 101 + setSeoData(extractedData); 102 + 103 + toast({ 104 + title: "Success", 105 + description: "SEO analysis completed successfully", 106 + }); 107 + } catch (err) { 108 + console.error('SEO Analysis Error:', err); 109 + let errorMessage = 'Failed to analyze website. '; 110 + 111 + if (err instanceof TypeError && err.message.includes('Invalid URL')) { 112 + errorMessage += 'Please enter a valid URL starting with http:// or https://'; 113 + } else if (err instanceof Error) { 114 + errorMessage += err.message; 115 + } else { 116 + errorMessage += 'Please check the URL and try again.'; 117 + } 118 + 119 + setError(errorMessage); 120 + toast({ 121 + title: "Error", 122 + description: errorMessage, 123 + variant: "destructive", 124 + }); 125 + } finally { 126 + setIsLoading(false); 127 + } 128 + }; 129 + 130 + const handleSubmit = (e: React.FormEvent) => { 131 + e.preventDefault(); 132 + analyzeSEO(); 133 + }; 134 + 135 + const getScoreColor = (hasValue: boolean) => { 136 + return hasValue ? 'success' : 'destructive'; 137 + }; 138 + 139 + 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> 147 + </div> 148 + ); 149 + 150 + return ( 151 + <div className="min-h-screen bg-gradient-subtle"> 152 + <div className="container mx-auto px-4 py-8"> 153 + <div className="max-w-4xl mx-auto"> 154 + {/* 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 159 + </p> 160 + </div> 161 + 162 + {/* URL Input Form */} 163 + <Card className="mb-8 shadow-card"> 164 + <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 + /> 175 + </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> 185 + </form> 186 + </CardContent> 187 + </Card> 188 + 189 + {/* Error Message */} 190 + {error && ( 191 + <Card className="mb-8 border-destructive"> 192 + <CardContent className="pt-6"> 193 + <div className="flex items-center gap-2 text-destructive"> 194 + <AlertCircle className="h-5 w-5" /> 195 + <span>{error}</span> 196 + </div> 197 + </CardContent> 198 + </Card> 199 + )} 200 + 201 + {/* Results */} 202 + {seoData && ( 203 + <div className="space-y-6"> 204 + {/* SEO Score Overview */} 205 + <Card className="shadow-card"> 206 + <CardHeader> 207 + <CardTitle className="flex items-center gap-2"> 208 + <Tag className="h-5 w-5" /> 209 + SEO Overview 210 + </CardTitle> 211 + </CardHeader> 212 + <CardContent> 213 + <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> 214 + <ScoreIndicator hasValue={!!seoData.title} label="Page Title" /> 215 + <ScoreIndicator hasValue={!!seoData.description} label="Meta Description" /> 216 + <ScoreIndicator hasValue={!!seoData.ogImage} label="OG Image" /> 217 + <ScoreIndicator hasValue={!!seoData.canonical} label="Canonical URL" /> 218 + </div> 219 + </CardContent> 220 + </Card> 221 + 222 + {/* Basic SEO */} 223 + <Card className="shadow-card"> 224 + <CardHeader> 225 + <CardTitle className="flex items-center gap-2"> 226 + <FileText className="h-5 w-5" /> 227 + Basic SEO 228 + </CardTitle> 229 + </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> 234 + {seoData.title && ( 235 + <p className="text-xs text-muted-foreground mt-1"> 236 + Length: {seoData.title.length} characters 237 + </p> 238 + )} 239 + </div> 240 + 241 + <Separator /> 242 + 243 + <div> 244 + <label className="text-sm font-medium text-muted-foreground">Meta Description</label> 245 + <p className="mt-1 text-foreground">{seoData.description || 'Not found'}</p> 246 + {seoData.description && ( 247 + <p className="text-xs text-muted-foreground mt-1"> 248 + Length: {seoData.description.length} characters 249 + </p> 250 + )} 251 + </div> 252 + 253 + {seoData.keywords && ( 254 + <> 255 + <Separator /> 256 + <div> 257 + <label className="text-sm font-medium text-muted-foreground">Keywords</label> 258 + <p className="mt-1 text-foreground">{seoData.keywords}</p> 259 + </div> 260 + </> 261 + )} 262 + 263 + {seoData.canonical && ( 264 + <> 265 + <Separator /> 266 + <div> 267 + <label className="text-sm font-medium text-muted-foreground">Canonical URL</label> 268 + <div className="flex items-center gap-2 mt-1"> 269 + <p className="text-foreground break-all">{seoData.canonical}</p> 270 + <Button variant="ghost" size="sm" asChild> 271 + <a href={seoData.canonical} target="_blank" rel="noopener noreferrer"> 272 + <ExternalLink className="h-3 w-3" /> 273 + </a> 274 + </Button> 275 + </div> 276 + </div> 277 + </> 278 + )} 279 + </CardContent> 280 + </Card> 281 + 282 + {/* Open Graph */} 283 + <Card className="shadow-card"> 284 + <CardHeader> 285 + <CardTitle className="flex items-center gap-2"> 286 + <Globe className="h-5 w-5" /> 287 + Open Graph (Facebook) 288 + </CardTitle> 289 + </CardHeader> 290 + <CardContent className="space-y-4"> 291 + {seoData.ogImage && ( 292 + <div> 293 + <label className="text-sm font-medium text-muted-foreground">OG Image</label> 294 + <div className="mt-2"> 295 + <img 296 + src={seoData.ogImage} 297 + alt="OG Preview" 298 + className="max-w-sm h-auto border rounded-md shadow-sm" 299 + onError={(e) => { 300 + e.currentTarget.style.display = 'none'; 301 + }} 302 + /> 303 + <p className="text-xs text-muted-foreground mt-1 break-all">{seoData.ogImage}</p> 304 + </div> 305 + </div> 306 + )} 307 + 308 + <div> 309 + <label className="text-sm font-medium text-muted-foreground">OG Title</label> 310 + <p className="mt-1 text-foreground">{seoData.ogTitle || 'Not found'}</p> 311 + </div> 312 + 313 + <div> 314 + <label className="text-sm font-medium text-muted-foreground">OG Description</label> 315 + <p className="mt-1 text-foreground">{seoData.ogDescription || 'Not found'}</p> 316 + </div> 317 + 318 + {seoData.ogType && ( 319 + <div> 320 + <label className="text-sm font-medium text-muted-foreground">OG Type</label> 321 + <Badge variant="secondary" className="mt-1">{seoData.ogType}</Badge> 322 + </div> 323 + )} 324 + </CardContent> 325 + </Card> 326 + 327 + {/* Twitter Cards */} 328 + <Card className="shadow-card"> 329 + <CardHeader> 330 + <CardTitle className="flex items-center gap-2"> 331 + <Image className="h-5 w-5" /> 332 + Twitter Cards 333 + </CardTitle> 334 + </CardHeader> 335 + <CardContent className="space-y-4"> 336 + {seoData.twitterCard && ( 337 + <div> 338 + <label className="text-sm font-medium text-muted-foreground">Card Type</label> 339 + <Badge variant="secondary" className="mt-1">{seoData.twitterCard}</Badge> 340 + </div> 341 + )} 342 + 343 + <div> 344 + <label className="text-sm font-medium text-muted-foreground">Twitter Title</label> 345 + <p className="mt-1 text-foreground">{seoData.twitterTitle || 'Not found'}</p> 346 + </div> 347 + 348 + <div> 349 + <label className="text-sm font-medium text-muted-foreground">Twitter Description</label> 350 + <p className="mt-1 text-foreground">{seoData.twitterDescription || 'Not found'}</p> 351 + </div> 352 + 353 + {seoData.twitterImage && ( 354 + <div> 355 + <label className="text-sm font-medium text-muted-foreground">Twitter Image</label> 356 + <div className="mt-2"> 357 + <img 358 + src={seoData.twitterImage} 359 + alt="Twitter Preview" 360 + className="max-w-sm h-auto border rounded-md shadow-sm" 361 + onError={(e) => { 362 + e.currentTarget.style.display = 'none'; 363 + }} 364 + /> 365 + <p className="text-xs text-muted-foreground mt-1 break-all">{seoData.twitterImage}</p> 366 + </div> 367 + </div> 368 + )} 369 + </CardContent> 370 + </Card> 371 + </div> 372 + )} 373 + </div> 374 + </div> 375 + </div> 376 + ); 377 + }; 378 + 379 + export default SEOTester;
+60 -36
src/index.css
··· 8 8 9 9 @layer base { 10 10 :root { 11 - --background: 0 0% 100%; 12 - --foreground: 222.2 84% 4.9%; 11 + --background: 210 20% 98%; 12 + --foreground: 215 25% 27%; 13 13 14 14 --card: 0 0% 100%; 15 - --card-foreground: 222.2 84% 4.9%; 15 + --card-foreground: 215 25% 27%; 16 16 17 17 --popover: 0 0% 100%; 18 - --popover-foreground: 222.2 84% 4.9%; 18 + --popover-foreground: 215 25% 27%; 19 19 20 - --primary: 222.2 47.4% 11.2%; 21 - --primary-foreground: 210 40% 98%; 20 + --primary: 214 84% 56%; 21 + --primary-foreground: 0 0% 100%; 22 + --primary-glow: 213 93% 67%; 22 23 23 - --secondary: 210 40% 96.1%; 24 - --secondary-foreground: 222.2 47.4% 11.2%; 24 + --secondary: 210 40% 96%; 25 + --secondary-foreground: 215 25% 27%; 25 26 26 - --muted: 210 40% 96.1%; 27 - --muted-foreground: 215.4 16.3% 46.9%; 27 + --muted: 210 40% 96%; 28 + --muted-foreground: 215 16% 47%; 28 29 29 - --accent: 210 40% 96.1%; 30 - --accent-foreground: 222.2 47.4% 11.2%; 30 + --accent: 213 27% 84%; 31 + --accent-foreground: 215 25% 27%; 31 32 32 - --destructive: 0 84.2% 60.2%; 33 - --destructive-foreground: 210 40% 98%; 33 + --destructive: 0 84% 60%; 34 + --destructive-foreground: 0 0% 100%; 34 35 35 - --border: 214.3 31.8% 91.4%; 36 - --input: 214.3 31.8% 91.4%; 37 - --ring: 222.2 84% 4.9%; 36 + --border: 214 32% 91%; 37 + --input: 214 32% 91%; 38 + --ring: 214 84% 56%; 39 + 40 + --success: 142 71% 45%; 41 + --success-foreground: 0 0% 100%; 42 + 43 + /* Gradients */ 44 + --gradient-primary: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-glow))); 45 + --gradient-subtle: linear-gradient(180deg, hsl(var(--background)), hsl(210 40% 95%)); 46 + 47 + /* Shadows */ 48 + --shadow-elegant: 0 10px 30px -10px hsl(var(--primary) / 0.2); 49 + --shadow-card: 0 4px 12px -2px hsl(215 25% 27% / 0.08); 38 50 39 51 --radius: 0.5rem; 40 52 ··· 56 68 } 57 69 58 70 .dark { 59 - --background: 222.2 84% 4.9%; 60 - --foreground: 210 40% 98%; 71 + --background: 215 28% 17%; 72 + --foreground: 210 20% 98%; 61 73 62 - --card: 222.2 84% 4.9%; 63 - --card-foreground: 210 40% 98%; 74 + --card: 215 28% 17%; 75 + --card-foreground: 210 20% 98%; 64 76 65 - --popover: 222.2 84% 4.9%; 66 - --popover-foreground: 210 40% 98%; 77 + --popover: 215 28% 17%; 78 + --popover-foreground: 210 20% 98%; 67 79 68 - --primary: 210 40% 98%; 69 - --primary-foreground: 222.2 47.4% 11.2%; 80 + --primary: 213 93% 67%; 81 + --primary-foreground: 215 28% 17%; 82 + --primary-glow: 214 84% 56%; 70 83 71 - --secondary: 217.2 32.6% 17.5%; 72 - --secondary-foreground: 210 40% 98%; 84 + --secondary: 215 25% 27%; 85 + --secondary-foreground: 210 20% 98%; 73 86 74 - --muted: 217.2 32.6% 17.5%; 75 - --muted-foreground: 215 20.2% 65.1%; 87 + --muted: 215 25% 27%; 88 + --muted-foreground: 217 10% 64%; 76 89 77 - --accent: 217.2 32.6% 17.5%; 78 - --accent-foreground: 210 40% 98%; 90 + --accent: 215 25% 27%; 91 + --accent-foreground: 210 20% 98%; 79 92 80 - --destructive: 0 62.8% 30.6%; 81 - --destructive-foreground: 210 40% 98%; 93 + --destructive: 0 63% 31%; 94 + --destructive-foreground: 210 20% 98%; 82 95 83 - --border: 217.2 32.6% 17.5%; 84 - --input: 217.2 32.6% 17.5%; 85 - --ring: 212.7 26.8% 83.9%; 96 + --success: 142 71% 45%; 97 + --success-foreground: 210 20% 98%; 98 + 99 + --border: 215 25% 27%; 100 + --input: 215 25% 27%; 101 + --ring: 213 93% 67%; 102 + 103 + /* Gradients */ 104 + --gradient-primary: linear-gradient(135deg, hsl(var(--primary)), hsl(var(--primary-glow))); 105 + --gradient-subtle: linear-gradient(180deg, hsl(var(--background)), hsl(215 25% 22%)); 106 + 107 + /* Shadows */ 108 + --shadow-elegant: 0 10px 30px -10px hsl(var(--primary) / 0.3); 109 + --shadow-card: 0 4px 12px -2px hsl(0 0% 0% / 0.25); 86 110 --sidebar-background: 240 5.9% 10%; 87 111 --sidebar-foreground: 240 4.8% 95.9%; 88 112 --sidebar-primary: 224.3 76.3% 48%;
+2 -9
src/pages/Index.tsx
··· 1 - // Update this page (the content is just a fallback if you fail to update the page) 1 + import SEOTester from '@/components/SEOTester'; 2 2 3 3 const Index = () => { 4 - return ( 5 - <div className="flex min-h-screen items-center justify-center bg-background"> 6 - <div className="text-center"> 7 - <h1 className="mb-4 text-4xl font-bold">Welcome to Your Blank App</h1> 8 - <p className="text-xl text-muted-foreground">Start building your amazing project here!</p> 9 - </div> 10 - </div> 11 - ); 4 + return <SEOTester />; 12 5 }; 13 6 14 7 export default Index;
+13
tailwind.config.ts
··· 22 22 primary: { 23 23 DEFAULT: "hsl(var(--primary))", 24 24 foreground: "hsl(var(--primary-foreground))", 25 + glow: "hsl(var(--primary-glow))", 25 26 }, 26 27 secondary: { 27 28 DEFAULT: "hsl(var(--secondary))", ··· 30 31 destructive: { 31 32 DEFAULT: "hsl(var(--destructive))", 32 33 foreground: "hsl(var(--destructive-foreground))", 34 + }, 35 + success: { 36 + DEFAULT: "hsl(var(--success))", 37 + foreground: "hsl(var(--success-foreground))", 33 38 }, 34 39 muted: { 35 40 DEFAULT: "hsl(var(--muted))", ··· 57 62 border: "hsl(var(--sidebar-border))", 58 63 ring: "hsl(var(--sidebar-ring))", 59 64 }, 65 + }, 66 + backgroundImage: { 67 + 'gradient-primary': 'var(--gradient-primary)', 68 + 'gradient-subtle': 'var(--gradient-subtle)', 69 + }, 70 + boxShadow: { 71 + 'elegant': 'var(--shadow-elegant)', 72 + 'card': 'var(--shadow-card)', 60 73 }, 61 74 borderRadius: { 62 75 lg: "var(--radius)",