Files changed (1) hide show
  1. app.py +548 -509
app.py CHANGED
@@ -1,533 +1,572 @@
1
- import streamlit as st
2
- import requests
3
- from urllib.parse import urlparse
4
- import time
5
- from seo_analyzer import SEOAnalyzer
6
- from preview_generators import PreviewGenerator
7
 
8
- # Page configuration
9
- st.set_page_config(
10
- page_title="SEO Metadata Analyzer",
11
- page_icon="🔍",
12
- layout="wide",
13
- initial_sidebar_state="expanded"
14
- )
15
 
16
- def validate_url(url):
17
- """Validate and normalize URL"""
18
- if not url:
19
- return None, "Please enter a URL"
20
 
21
- if not url.startswith(('http://', 'https://')):
22
- url = 'https://' + url
23
 
24
- try:
25
- result = urlparse(url)
26
- if not all([result.scheme, result.netloc]):
27
- return None, "Invalid URL format"
28
- return url, None
29
- except Exception as e:
30
- return None, f"Invalid URL: {str(e)}"
31
-
32
- def main():
33
- # Custom CSS for responsive design
34
- st.markdown("""
35
- <style>
36
- /* Responsive Main Header */
37
- .main-header {
38
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
39
- padding: 2rem 1rem;
40
- border-radius: 10px;
41
- margin-bottom: 2rem;
42
- text-align: center;
43
- }
44
- .main-header h1 {
45
- color: white;
46
- margin: 0;
47
- font-size: clamp(1.8rem, 4vw, 2.5rem);
48
- font-weight: 700;
49
- }
50
- .main-header p {
51
- color: #f0f0f0;
52
- margin: 0.5rem 0 0 0;
53
- font-size: clamp(1rem, 2.5vw, 1.2rem);
54
- line-height: 1.4;
55
  }
56
 
57
- /* Responsive Buttons */
58
- .stButton > button {
59
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
60
- color: white;
61
- border: none;
62
- border-radius: 12px;
63
- font-weight: 600;
64
- width: 100%;
65
- padding: clamp(12px, 3vw, 16px) clamp(16px, 4vw, 24px);
66
- font-size: clamp(14px, 3.5vw, 16px);
67
- transition: all 0.3s ease;
68
- margin: 8px 0;
69
- min-height: 44px;
70
- display: flex;
71
- align-items: center;
72
- justify-content: center;
73
- text-align: center;
74
- }
75
- .stButton > button:hover {
76
- transform: translateY(-1px);
77
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
78
- background: linear-gradient(90deg, #5a6fd8 0%, #6a4190 100%);
 
 
 
 
 
 
79
  }
80
- </style>
81
- """, unsafe_allow_html=True)
82
 
83
- # Main header
84
- st.markdown("""
85
- <div class="main-header">
86
- <h1>🔍 SEO Metadata Analyzer</h1>
87
- <p>Analyze your website's SEO performance and get actionable insights with visual previews</p>
88
- </div>
89
- """, unsafe_allow_html=True)
90
 
91
- # Sidebar for controls
92
- with st.sidebar:
93
- st.markdown("### 🎯 Analysis Controls")
94
-
95
- # Enhanced URL input with visual https:// prefix
96
- st.markdown("**🌐 Website URL**")
97
-
98
- # Create a visual input with https:// prefix
99
- domain_input = st.text_input(
100
- "Domain",
101
- placeholder="google.com or github.com",
102
- help="Just enter the domain name - https:// is added automatically!",
103
- key="domain_input"
104
- )
105
-
106
- # Display the full URL visually
107
- if domain_input:
108
- url_input = f"https://{domain_input.strip().replace('https://', '').replace('http://', '')}"
109
- st.markdown(f"""
110
- <div style="
111
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
112
- color: white;
113
- padding: 0.5rem 1rem;
114
- border-radius: 8px;
115
- margin: 0.5rem 0;
116
- font-family: monospace;
117
- font-size: 0.9rem;
118
- word-break: break-all;
119
- ">
120
- 📎 Full URL: {url_input}
121
- </div>
122
- """, unsafe_allow_html=True)
123
- else:
124
- url_input = ""
125
-
126
- st.markdown("---")
127
-
128
- # Feature highlights
129
- st.markdown("""
130
- **🚀 What You'll Get:**
131
- - SEO Score & Status
132
- - Google Preview
133
- - Social Media Previews
134
- - Detailed Analysis
135
- - Action Items
136
- """)
137
-
138
- analyze_button = st.button("🚀 Analyze SEO", type="primary", use_container_width=True)
139
-
140
- if url_input:
141
- normalized_url, error = validate_url(url_input)
142
- if error:
143
- st.error(error)
144
- elif normalized_url:
145
- st.success(f"✅ Ready to analyze: {normalized_url}")
146
-
147
- st.markdown("---")
148
- st.markdown("💡 **Pro Tip:** Try analyzing popular sites like GitHub, Twitter, or LinkedIn to see examples of good SEO!")
 
 
 
 
149
 
150
- # Main content area
151
- if analyze_button and url_input:
152
- normalized_url, error = validate_url(url_input)
153
-
154
- if error:
155
- st.error(error)
156
- return
157
-
158
- # Progress indicator
159
- progress_bar = st.progress(0)
160
- status_text = st.empty()
161
-
162
- try:
163
- # Initialize analyzer
164
- status_text.text("🌐 Fetching website data...")
165
- progress_bar.progress(20)
166
-
167
- analyzer = SEOAnalyzer()
168
- analysis_result = analyzer.analyze_website(normalized_url)
169
-
170
- if not analysis_result['success']:
171
- st.error(f"❌ Failed to analyze website: {analysis_result['error']}")
172
- return
173
-
174
- progress_bar.progress(60)
175
- status_text.text("🔍 Analyzing SEO metadata...")
176
-
177
- # Generate previews
178
- progress_bar.progress(80)
179
- status_text.text("🎨 Generating previews...")
180
-
181
- preview_gen = PreviewGenerator(analysis_result['metadata'])
182
-
183
- progress_bar. me show you all the complete code files properly! I'll provide each file one by one:
184
 
185
- ## 📁 File 1: `app.py` (Main Application)
 
 
 
 
 
 
 
 
 
186
 
187
- ```python
188
- import streamlit as st
189
- import requests
190
- from urllib.parse import urlparse
191
- import time
192
- from seo_analyzer import SEOAnalyzer
193
- from preview_generators import PreviewGenerator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
194
 
195
- # Page configuration
196
- st.set_page_config(
197
- page_title="SEO Metadata Analyzer",
198
- page_icon="🔍",
199
- layout="wide",
200
- initial_sidebar_state="expanded"
201
- )
202
 
203
- def validate_url(url):
204
- """Validate and normalize URL"""
205
- if not url:
206
- return None, "Please enter a URL"
207
-
208
- if not url.startswith(('http://', 'https://')):
209
- url = 'https://' + url
210
-
211
- try:
212
- result = urlparse(url)
213
- if not all([result.scheme, result.netloc]):
214
- return None, "Invalid URL format"
215
- return url, None
216
- except Exception as e:
217
- return None, f"Invalid URL: {str(e)}"
218
 
219
- def main():
220
- # Custom CSS for responsive design
221
- st.markdown("""
222
- <style>
223
- /* Responsive Main Header */
224
- .main-header {
225
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
226
- padding: 2rem 1rem;
227
- border-radius: 10px;
228
- margin-bottom: 2rem;
229
- text-align: center;
230
- }
231
- .main-header h1 {
232
- color: white;
233
- margin: 0;
234
- font-size: clamp(1.8rem, 4vw, 2.5rem);
235
- font-weight: 700;
236
- }
237
- .main-header p {
238
- color: #f0f0f0;
239
- margin: 0.5rem 0 0 0;
240
- font-size: clamp(1rem, 2.5vw, 1.2rem);
241
- line-height: 1.4;
242
- }
243
-
244
- /* Responsive Buttons */
245
- .stButton > button {
246
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
247
- color: white;
248
- border: none;
249
- border-radius: 12px;
250
- font-weight: 600;
251
- width: 100%;
252
- padding: clamp(12px, 3vw, 16px) clamp(16px, 4vw, 24px);
253
- font-size: clamp(14px, 3.5vw, 16px);
254
- transition: all 0.3s ease;
255
- margin: 8px 0;
256
- min-height: 44px;
257
- display: flex;
258
- align-items: center;
259
- justify-content: center;
260
- text-align: center;
261
- }
262
- .stButton > button:hover {
263
- transform: translateY(-1px);
264
- box-shadow: 0 6px 20px rgba(102, 126, 234, 0.3);
265
- background: linear-gradient(90deg, #5a6fd8 0%, #6a4190 100%);
266
- }
267
- </style>
268
- """, unsafe_allow_html=True)
269
-
270
- # Main header
271
- st.markdown("""
272
- <div class="main-header">
273
- <h1>🔍 SEO Metadata Analyzer</h1>
274
- <p>Analyze your website's SEO performance and get actionable insights with visual previews</p>
275
  </div>
276
- """, unsafe_allow_html=True)
277
-
278
- # Sidebar for controls
279
- with st.sidebar:
280
- st.markdown("### 🎯 Analysis Controls")
281
-
282
- # Enhanced URL input with visual https:// prefix
283
- st.markdown("**🌐 Website URL**")
284
-
285
- # Create a visual input with https:// prefix
286
- domain_input = st.text_input(
287
- "Domain",
288
- placeholder="google.com or github.com",
289
- help="Just enter the domain name - https:// is added automatically!",
290
- key="domain_input"
291
- )
292
-
293
- # Display the full URL visually
294
- if domain_input:
295
- url_input = f"https://{domain_input.strip().replace('https://', '').replace('http://', '')}"
296
- st.markdown(f"""
297
- <div style="
298
- background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
299
- color: white;
300
- padding: 0.5rem 1rem;
301
- border-radius: 8px;
302
- margin: 0.5rem 0;
303
- font-family: monospace;
304
- font-size: 0.9rem;
305
- word-break: break-all;
306
- ">
307
- 📎 Full URL: {url_input}
 
 
 
 
 
 
 
 
 
 
308
  </div>
309
- """, unsafe_allow_html=True)
310
- else:
311
- url_input = ""
312
-
313
- st.markdown("---")
314
-
315
- # Feature highlights
316
- st.markdown("""
317
- **🚀 What You'll Get:**
318
- - SEO Score & Status
319
- - Google Preview
320
- - Social Media Previews
321
- - Detailed Analysis
322
- - Action Items
323
- """)
324
-
325
- analyze_button = st.button("🚀 Analyze SEO", type="primary", use_container_width=True)
326
-
327
- if url_input:
328
- normalized_url, error = validate_url(url_input)
329
- if error:
330
- st.error(error)
331
- elif normalized_url:
332
- st.success(f" Ready to analyze: {normalized_url}")
333
-
334
- st.markdown("---")
335
- st.markdown("💡 **Pro Tip:** Try analyzing popular sites like GitHub, Twitter, or LinkedIn to see examples of good SEO!")
336
-
337
- # Main content area
338
- if analyze_button and url_input:
339
- normalized_url, error = validate_url(url_input)
340
-
341
- if error:
342
- st.error(error)
343
- return
344
-
345
- # Progress indicator
346
- progress_bar = st.progress(0)
347
- status_text = st.empty()
348
-
349
- try:
350
- # Initialize analyzer
351
- status_text.text("🌐 Fetching website data...")
352
- progress_bar.progress(20)
353
-
354
- analyzer = SEOAnalyzer()
355
- analysis_result = analyzer.analyze_website(normalized_url)
356
-
357
- if not analysis_result['success']:
358
- st.error(f"❌ Failed to analyze website: {analysis_result['error']}")
359
- return
360
-
361
- progress_bar.progress(60)
362
- status_text.text("🔍 Analyzing SEO metadata...")
363
-
364
- # Generate previews
365
- progress_bar.progress(80)
366
- status_text.text("🎨 Generating previews...")
367
-
368
- preview_gen = PreviewGenerator(analysis_result['metadata'])
369
-
370
- progress_bar.progress(100)
371
- status_text.text("✅ Analysis complete!")
372
- results
373
- display_results(analysis_result, preview_gen, normalized_url)
374
-
375
- except Exception as e:
376
- st.error(f" An error occurred during analysis: {str(e)}")
377
- progress_bar.empty()
378
- status_text.empty()
379
-
380
- else:
381
- # Show welcome dashboard when no analysis is running
382
- col1, col2, col3 = st.columns([1, 2, 1])
383
- with col2:
384
- st.markdown("""
385
- <div style="
386
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
387
- padding: 2rem;
388
- border-radius: 15px;
389
- margin: 2rem 0;
390
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
391
- ">
392
- <h3 style="text-align: center; color: #2c3e50; margin-bottom: 1.5rem;">🎯 How It Works</h3>
393
-
394
- <div style="display: flex; align-items: center; margin-bottom: 1rem;">
395
- <div style="background: #3498db; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; margin-right: 1rem; font-weight: bold;">1</div>
396
- <span><strong>Enter a URL</strong> in the sidebar (e.g., google.com, github.com)</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
  </div>
398
-
399
- <div style="display: flex; align-items: center; margin-bottom: 1rem;">
400
- <div style="background: #e74c3c; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; margin-right: 1rem; font-weight: bold;">2</div>
401
- <span><strong>Click Analyze SEO</strong> to start the comprehensive analysis</span>
 
402
  </div>
403
-
404
- <div style="display: flex; align-items: center; margin-bottom: 1.5rem;">
405
- <div style="background: #27ae60; color: white; border-radius: 50%; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; margin-right: 1rem; font-weight: bold;">3</div>
406
- <span><strong>Review detailed results</strong> with actionable insights</span>
 
407
  </div>
408
-
409
- <h4 style="color: #2c3e50; margin-bottom: 1rem;">📊 Complete SEO Analysis Includes:</h4>
410
- <ul style="color: #34495e; line-height: 1.6;">
411
- <li><strong>SEO Score & Status</strong> - Overall website rating</li>
412
- <li><strong>Meta Tags Analysis</strong> - Title, description, keywords</li>
413
- <li><strong>Social Media Previews</strong> - Facebook, Twitter cards</li>
414
- <li><strong>Google Search Preview</strong> - How your site appears in search</li>
415
- <li><strong>Technical SEO Check</strong> - Viewport, canonical URLs, robots</li>
416
- <li><strong>Structured Data</strong> - JSON-LD schema markup</li>
417
- <li><strong>Priority Recommendations</strong> - Step-by-step improvement guide</li>
418
- </ul>
 
 
 
 
419
  </div>
420
- """, unsafe_allow_html=True)
421
-
422
- # Example websites to try
423
- st.markdown("### 🌟 Try These Popular Websites:")
424
-
425
- example_col1, example_col2, example_col3 = st.columns(3)
426
-
427
- with example_col1:
428
- if st.button("📚 GitHub", use_container_width=True):
429
- st.session_state.example_url = "github.com"
430
- st.rerun()
431
-
432
- with example_col2:
433
- if st.button("🔍 Google", use_container_width=True):
434
- st.session_state.example_url = "google.com"
435
- st.rerun()
436
-
437
- with example_col3:
438
- if st.button("💼 LinkedIn", use_container_width=True):
439
- st.session_state.example_url = "linkedin.com"
440
- st.rerun()
441
-
442
- # Handle example URL selection
443
- if hasattr(st.session_state, 'example_url'):
444
- st.info(f"💡 Example selected: {st.session_state.example_url} - Enter this in the sidebar and click Analyze!")
445
 
446
- def display_results(analysis_result, preview_gen, url):
447
- """Display the complete analysis results"""
448
- metadata = analysis_result['metadata']
449
- seo_score = analysis_result['seo_score']
450
- recommendations = analysis_result['recommendations']
451
-
452
- # Overall SEO Status Header
453
- st.header("🎯 SEO Analysis Report")
454
-
455
- # Overall Status Card
456
- if seo_score >= 80:
457
- status_color = "#28a745"
458
- status_text = "EXCELLENT"
459
- status_icon = "🟢"
460
- status_message = "Your website has excellent SEO optimization!"
461
- elif seo_score >= 60:
462
- status_color = "#ffc107"
463
- status_text = "GOOD"
464
- status_icon = "🟡"
465
- status_message = "Your website has good SEO with room for improvement."
466
- else:
467
- status_color = "#dc3545"
468
- status_text = "NEEDS IMPROVEMENT"
469
- status_icon = "🔴"
470
- status_message = "Your website needs significant SEO improvements."
471
-
472
- # Main Status Display
473
- st.markdown(f"""
474
- <div style="
475
- background: linear-gradient(135deg, {status_color}15, {status_color}05);
476
- border: 2px solid {status_color};
477
- border-radius: 15px;
478
- padding: clamp(15px, 5vw, 25px);
479
- margin: 20px auto;
480
- text-align: center;
481
- max-width: 100%;
482
- display: flex;
483
- flex-direction: column;
484
- align-items: center;
485
- justify-content: center;
486
- ">
487
- <h1 style="
488
- margin: 0;
489
- color: {status_color};
490
- font-size: clamp(2.5rem, 8vw, 4rem);
491
- font-weight: 800;
492
- line-height: 1;
493
- ">{seo_score}</h1>
494
- <h2 style="
495
- margin: clamp(8px, 2vw, 15px) 0;
496
- color: {status_color};
497
- font-size: clamp(1.2rem, 4vw, 1.8rem);
498
- font-weight: 600;
499
- ">{status_icon} {status_text}</h2>
500
- <p style="
501
- margin: 0;
502
- font-size: clamp(1rem, 3vw, 1.2rem);
503
- color: #666;
504
- line-height: 1.4;
505
- text-align: center;
506
- max-width: 90%;
507
- ">{status_message}</p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
508
  </div>
509
- """, unsafe_allow_html=True)
510
-
511
- # Previews
512
- st.header("👀 Search & Social Media Previews")
513
-
514
- # Google Search Preview
515
- st.subheader("🔍 Google Search Result Preview")
516
- google_preview = preview_gen.generate_google_preview(url)
517
- st.markdown(google_preview, unsafe_allow_html=True)
518
-
519
- # Social Media Previews
520
- col1, col2 = st.columns([1, 1])
521
-
522
- with col1:
523
- st.subheader("📘 Facebook Preview")
524
- facebook_preview = preview_gen.generate_facebook_preview(url)
525
- st.markdown(facebook_preview, unsafe_allow_html=True)
526
-
527
- with col2:
528
- st.subheader("🐦 Twitter Preview")
529
- twitter_preview = preview_gen.generate_twitter_preview(url)
530
- st.markdown(twitter_preview, unsafe_allow_html=True)
531
 
532
- if __name__ == "__main__":
533
- main()
 
1
+ import React, { useState } from 'react';
2
+ import { Search, CheckCircle, XCircle, AlertCircle, Eye, Share2, Globe, Info } from 'lucide-react';
 
 
 
 
3
 
4
+ const SEOAnalyzer = () => {
5
+ const [url, setUrl] = useState('');
6
+ const [loading, setLoading] = useState(false);
7
+ const [results, setResults] = useState(null);
8
+ const [activePreview, setActivePreview] = useState('google');
 
 
9
 
10
+ const analyzeSEO = async () => {
11
+ if (!url) return;
 
 
12
 
13
+ setLoading(true);
14
+ setResults(null);
15
 
16
+ // Ensure URL has protocol
17
+ let targetUrl = url;
18
+ if (!targetUrl.startsWith('http://') && !targetUrl.startsWith('https://')) {
19
+ targetUrl = 'https://' + targetUrl;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  }
21
 
22
+ try {
23
+ // Try to fetch using a more reliable CORS proxy
24
+ const response = await fetch(`https://api.codetabs.com/v1/proxy?quest=${encodeURIComponent(targetUrl)}`, {
25
+ method: 'GET',
26
+ headers: {
27
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
28
+ }
29
+ });
30
+
31
+ if (!response.ok) {
32
+ throw new Error(`HTTP ${response.status}`);
33
+ }
34
+
35
+ const html = await response.text();
36
+
37
+ if (html && html.length > 100) {
38
+ const parser = new DOMParser();
39
+ const doc = parser.parseFromString(html, 'text/html');
40
+ const analysis = extractMetaTags(doc, targetUrl);
41
+ setResults(analysis);
42
+ } else {
43
+ throw new Error('Empty or invalid response');
44
+ }
45
+ } catch (error) {
46
+ console.error('Fetch failed:', error);
47
+ // Show demo data with actual analysis
48
+ const demoAnalysis = createDemoAnalysis(targetUrl);
49
+ setResults(demoAnalysis);
50
  }
 
 
51
 
52
+ setLoading(false);
53
+ };
54
+
55
+ const createDemoAnalysis = (url) => {
56
+ // Create realistic demo data that shows both good and bad examples
57
+ const domain = url.replace(/(https?:\/\/)?(www\.)?/, '').split('/')[0];
 
58
 
59
+ return {
60
+ basic: {
61
+ title: `${domain.charAt(0).toUpperCase() + domain.slice(1)} - Home Page`,
62
+ description: `Welcome to ${domain}. We provide excellent services and solutions for your needs. Visit us today!`,
63
+ keywords: 'business, services, solutions, quality',
64
+ robots: 'index, follow',
65
+ canonical: url
66
+ },
67
+ openGraph: {
68
+ title: `${domain.charAt(0).toUpperCase() + domain.slice(1)} - Social Title`,
69
+ description: `Discover amazing content and services at ${domain}. Join thousands of satisfied customers!`,
70
+ image: `https://via.placeholder.com/1200x630/3B82F6/ffffff?text=${encodeURIComponent(domain)}`,
71
+ url: url,
72
+ type: 'website'
73
+ },
74
+ twitter: {
75
+ card: 'summary_large_image',
76
+ title: `${domain} - Twitter Optimized`,
77
+ description: `Follow ${domain} for the latest updates and exclusive content!`,
78
+ image: `https://via.placeholder.com/1200x600/1DA1F2/ffffff?text=${encodeURIComponent(domain)}`
79
+ },
80
+ structuredData: [
81
+ {
82
+ "@context": "https://schema.org",
83
+ "@type": "Organization",
84
+ "name": domain,
85
+ "url": url,
86
+ "description": `Official website of ${domain}`
87
+ }
88
+ ],
89
+ url,
90
+ isDemo: true,
91
+ issues: [
92
+ 'Title could be more descriptive',
93
+ 'Meta description is too generic',
94
+ 'Missing important Open Graph tags',
95
+ 'No Twitter card optimization'
96
+ ]
97
+ };
98
+ };
99
+
100
+ const extractMetaTags = (doc, url) => {
101
+ const getMetaContent = (name, property = false) => {
102
+ const selector = property ? `meta[property="${name}"]` : `meta[name="${name}"]`;
103
+ const meta = doc.querySelector(selector);
104
+ return meta ? meta.getAttribute('content') : null;
105
+ };
106
+
107
+ const title = doc.querySelector('title')?.textContent?.trim() || '';
108
+ const description = getMetaContent('description');
109
+ const keywords = getMetaContent('keywords');
110
+ const robots = getMetaContent('robots');
111
+ const canonical = doc.querySelector('link[rel="canonical"]')?.getAttribute('href') || '';
112
+ const viewport = getMetaContent('viewport');
113
+
114
+ // Open Graph tags
115
+ const ogTitle = getMetaContent('og:title', true);
116
+ const ogDescription = getMetaContent('og:description', true);
117
+ const ogImage = getMetaContent('og:image', true);
118
+ const ogUrl = getMetaContent('og:url', true);
119
+ const ogType = getMetaContent('og:type', true);
120
+ const ogSiteName = getMetaContent('og:site_name', true);
121
 
122
+ // Twitter Card tags
123
+ const twitterCard = getMetaContent('twitter:card');
124
+ const twitterTitle = getMetaContent('twitter:title');
125
+ const twitterDescription = getMetaContent('twitter:description');
126
+ const twitterImage = getMetaContent('twitter:image');
127
+ const twitterSite = getMetaContent('twitter:site');
128
+
129
+ // Structured data
130
+ const jsonLd = Array.from(doc.querySelectorAll('script[type="application/ld+json"]'))
131
+ .map(script => {
132
+ try {
133
+ return JSON.parse(script.textContent);
134
+ } catch {
135
+ return null;
136
+ }
137
+ }).filter(Boolean);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ // Generate SEO issues and recommendations
140
+ const issues = [];
141
+ if (!title || title.length < 30) issues.push('Title tag is too short (should be 30-60 characters)');
142
+ if (title && title.length > 60) issues.push('Title tag is too long (should be 30-60 characters)');
143
+ if (!description) issues.push('Missing meta description');
144
+ if (description && description.length < 120) issues.push('Meta description is too short (should be 120-160 characters)');
145
+ if (description && description.length > 160) issues.push('Meta description is too long (should be 120-160 characters)');
146
+ if (!ogTitle) issues.push('Missing Open Graph title');
147
+ if (!ogImage) issues.push('Missing Open Graph image');
148
+ if (!twitterCard) issues.push('Missing Twitter Card type');
149
 
150
+ return {
151
+ basic: {
152
+ title,
153
+ description,
154
+ keywords,
155
+ robots,
156
+ canonical,
157
+ viewport
158
+ },
159
+ openGraph: {
160
+ title: ogTitle,
161
+ description: ogDescription,
162
+ image: ogImage,
163
+ url: ogUrl,
164
+ type: ogType,
165
+ siteName: ogSiteName
166
+ },
167
+ twitter: {
168
+ card: twitterCard,
169
+ title: twitterTitle,
170
+ description: twitterDescription,
171
+ image: twitterImage,
172
+ site: twitterSite
173
+ },
174
+ structuredData: jsonLd,
175
+ url,
176
+ issues,
177
+ isDemo: false
178
+ };
179
+ };
180
 
181
+ const getStatus = (value, minLength = 0, maxLength = 999) => {
182
+ if (!value) return 'error';
183
+ if (value.length < minLength || value.length > maxLength) return 'warning';
184
+ return 'success';
185
+ };
 
 
186
 
187
+ const StatusIcon = ({ status }) => {
188
+ if (status === 'success') return <CheckCircle className="w-5 h-5 text-green-500" />;
189
+ if (status === 'error') return <XCircle className="w-5 h-5 text-red-500" />;
190
+ return <AlertCircle className="w-5 h-5 text-yellow-500" />;
191
+ };
 
 
 
 
 
 
 
 
 
 
192
 
193
+ const MetaTagRow = ({ label, value, status, recommendation, charCount }) => (
194
+ <div className="flex items-start gap-3 p-4 bg-gray-50 rounded-lg">
195
+ <StatusIcon status={status} />
196
+ <div className="flex-1">
197
+ <div className="flex justify-between items-start">
198
+ <h4 className="font-semibold text-gray-800">{label}</h4>
199
+ {charCount && value && (
200
+ <span className={`text-xs px-2 py-1 rounded ${
201
+ status === 'success' ? 'bg-green-100 text-green-700' :
202
+ status === 'warning' ? 'bg-yellow-100 text-yellow-700' :
203
+ 'bg-red-100 text-red-700'
204
+ }`}>
205
+ {value.length} chars
206
+ </span>
207
+ )}
208
+ </div>
209
+ <p className="text-sm text-gray-600 mt-1 break-words">
210
+ {value || <span className="text-red-500">Not found</span>}
211
+ </p>
212
+ {recommendation && (
213
+ <p className="text-xs text-gray-500 mt-2">{recommendation}</p>
214
+ )}
215
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
  </div>
217
+ );
218
+
219
+ const GooglePreview = ({ data }) => (
220
+ <div className="bg-white p-6 rounded-lg border shadow-sm">
221
+ <h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
222
+ <Globe className="w-5 h-5 text-blue-600" />
223
+ Google Search Preview
224
+ </h3>
225
+ <div className="max-w-2xl">
226
+ <div className="text-sm text-green-700 mb-1">{data.url}</div>
227
+ <div className="text-xl text-blue-600 hover:underline cursor-pointer mb-1 line-clamp-2">
228
+ {data.basic.title || 'Untitled Page'}
229
+ </div>
230
+ <div className="text-sm text-gray-700 leading-relaxed line-clamp-3">
231
+ {data.basic.description || 'No description available for this page.'}
232
+ </div>
233
+ </div>
234
+ </div>
235
+ );
236
+
237
+ const FacebookPreview = ({ data }) => {
238
+ const title = data.openGraph.title || data.basic.title;
239
+ const description = data.openGraph.description || data.basic.description;
240
+ const image = data.openGraph.image;
241
+
242
+ return (
243
+ <div className="bg-white p-6 rounded-lg border shadow-sm">
244
+ <h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
245
+ <Share2 className="w-5 h-5 text-blue-600" />
246
+ Facebook Preview
247
+ </h3>
248
+ <div className="border rounded-lg overflow-hidden max-w-lg">
249
+ {image && (
250
+ <div className="h-64 bg-gray-200 flex items-center justify-center overflow-hidden">
251
+ <img
252
+ src={image}
253
+ alt="Preview"
254
+ className="w-full h-full object-cover"
255
+ onError={(e) => {
256
+ e.target.src = 'https://via.placeholder.com/600x315/E5E7EB/6B7280?text=Image+Not+Found';
257
+ }}
258
+ />
259
  </div>
260
+ )}
261
+ <div className="p-4">
262
+ <div className="text-xs text-gray-500 uppercase mb-2">{new URL(data.url).hostname}</div>
263
+ <div className="font-semibold text-gray-900 mb-2 line-clamp-2">
264
+ {title || 'Untitled'}
265
+ </div>
266
+ <div className="text-sm text-gray-600 line-clamp-2">
267
+ {description || 'No description available.'}
268
+ </div>
269
+ </div>
270
+ </div>
271
+ </div>
272
+ );
273
+ };
274
+
275
+ const TwitterPreview = ({ data }) => {
276
+ const title = data.twitter.title || data.openGraph.title || data.basic.title;
277
+ const description = data.twitter.description || data.openGraph.description || data.basic.description;
278
+ const image = data.twitter.image || data.openGraph.image;
279
+
280
+ return (
281
+ <div className="bg-white p-6 rounded-lg border shadow-sm">
282
+ <h3 className="text-lg font-semibold mb-4 flex items-center gap-2">
283
+ <Share2 className="w-5 h-5 text-blue-400" />
284
+ Twitter Preview
285
+ </h3>
286
+ <div className="border rounded-2xl overflow-hidden max-w-md bg-white">
287
+ {image && (
288
+ <div className="h-48 bg-gray-200 flex items-center justify-center overflow-hidden">
289
+ <img
290
+ src={image}
291
+ alt="Preview"
292
+ className="w-full h-full object-cover"
293
+ onError={(e) => {
294
+ e.target.src = 'https://via.placeholder.com/600x300/E5E7EB/6B7280?text=Image+Not+Found';
295
+ }}
296
+ />
297
+ </div>
298
+ )}
299
+ <div className="p-4">
300
+ <div className="font-semibold text-gray-900 mb-1 line-clamp-2">
301
+ {title || 'Untitled'}
302
+ </div>
303
+ <div className="text-sm text-gray-600 mb-2 line-clamp-2">
304
+ {description || 'No description available.'}
305
+ </div>
306
+ <div className="text-xs text-gray-500">{new URL(data.url).hostname}</div>
307
+ </div>
308
+ </div>
309
+ </div>
310
+ );
311
+ };
312
+
313
+ // Auto-run demo on component mount
314
+ React.useEffect(() => {
315
+ setUrl('https://example.com');
316
+ // Auto-analyze example.com for demonstration
317
+ setTimeout(() => {
318
+ const demoData = createDemoAnalysis('https://example.com');
319
+ setResults(demoData);
320
+ }, 1000);
321
+ }, []);
322
+
323
+ return (
324
+ <div className="min-h-screen bg-gradient-to-br from-blue-50 via-indigo-50 to-purple-50 p-4">
325
+ <div className="max-w-6xl mx-auto">
326
+ <div className="text-center mb-8">
327
+ <h1 className="text-4xl font-bold bg-gradient-to-r from-blue-600 to-indigo-600 bg-clip-text text-transparent mb-4">
328
+ SEO Meta Tags Analyzer
329
+ </h1>
330
+ <p className="text-lg text-gray-600 max-w-2xl mx-auto">
331
+ Analyze and optimize your website's SEO meta tags with instant visual previews for Google, Facebook, and Twitter
332
+ </p>
333
+ </div>
334
+
335
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 mb-8 border border-white/20">
336
+ <div className="flex gap-4 items-center">
337
+ <input
338
+ type="url"
339
+ value={url}
340
+ onChange={(e) => setUrl(e.target.value)}
341
+ placeholder="Enter website URL (e.g., https://example.com)"
342
+ 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"
343
+ />
344
+ <button
345
+ onClick={analyzeSEO}
346
+ disabled={loading || !url}
347
+ 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"
348
+ >
349
+ {loading ? (
350
+ <div className="w-5 h-5 border-2 border-white border-t-transparent rounded-full animate-spin" />
351
+ ) : (
352
+ <Search className="w-5 h-5" />
353
+ )}
354
+ {loading ? 'Analyzing...' : 'Analyze SEO'}
355
+ </button>
356
+ </div>
357
+ </div>
358
+
359
+ {results?.isDemo && (
360
+ <div className="bg-gradient-to-r from-blue-50 to-indigo-50 border border-blue-200 rounded-xl p-4 mb-8">
361
+ <div className="flex items-start gap-3">
362
+ <Info className="w-5 h-5 text-blue-600 mt-0.5" />
363
+ <div>
364
+ <p className="text-blue-800 font-medium">
365
+ Demo Analysis Active
366
+ </p>
367
+ <p className="text-sm text-blue-700 mt-1">
368
+ Due to browser security restrictions, this shows example data. In production, deploy server-side to analyze any website.
369
+ </p>
370
+ </div>
371
+ </div>
372
+ </div>
373
+ )}
374
+
375
+ {results && (
376
+ <div className="space-y-8">
377
+ {/* SEO Score Overview */}
378
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20">
379
+ <h2 className="text-2xl font-bold text-gray-800 mb-6">SEO Analysis Overview</h2>
380
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
381
+ <div className="text-center p-4 bg-gradient-to-br from-green-50 to-emerald-50 rounded-xl">
382
+ <div className="text-3xl font-bold text-green-600">
383
+ {results.issues ? Math.max(0, 100 - (results.issues.length * 15)) : 85}
384
+ </div>
385
+ <div className="text-sm text-green-700 font-medium">SEO Score</div>
386
  </div>
387
+ <div className="text-center p-4 bg-gradient-to-br from-blue-50 to-cyan-50 rounded-xl">
388
+ <div className="text-3xl font-bold text-blue-600">
389
+ {results.structuredData?.length || 0}
390
+ </div>
391
+ <div className="text-sm text-blue-700 font-medium">Structured Data</div>
392
  </div>
393
+ <div className="text-center p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-xl">
394
+ <div className="text-3xl font-bold text-purple-600">
395
+ {results.issues?.length || 0}
396
+ </div>
397
+ <div className="text-sm text-purple-700 font-medium">Issues Found</div>
398
  </div>
399
+ </div>
400
+
401
+ {results.issues && results.issues.length > 0 && (
402
+ <div className="mt-6 p-4 bg-yellow-50 rounded-xl">
403
+ <h3 className="font-semibold text-yellow-800 mb-2">Recommendations:</h3>
404
+ <ul className="space-y-1">
405
+ {results.issues.map((issue, index) => (
406
+ <li key={index} className="text-sm text-yellow-700 flex items-center gap-2">
407
+ <AlertCircle className="w-4 h-4" />
408
+ {issue}
409
+ </li>
410
+ ))}
411
+ </ul>
412
+ </div>
413
+ )}
414
  </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
415
 
416
+ {/* Basic SEO Tags */}
417
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20">
418
+ <h2 className="text-2xl font-bold text-gray-800 mb-6">Basic SEO Tags</h2>
419
+ <div className="space-y-4">
420
+ <MetaTagRow
421
+ label="Title Tag"
422
+ value={results.basic.title}
423
+ status={getStatus(results.basic.title, 30, 60)}
424
+ recommendation="Should be 30-60 characters and include your main keyword."
425
+ charCount={true}
426
+ />
427
+ <MetaTagRow
428
+ label="Meta Description"
429
+ value={results.basic.description}
430
+ status={getStatus(results.basic.description, 120, 160)}
431
+ recommendation="Should be 120-160 characters and compelling to encourage clicks."
432
+ charCount={true}
433
+ />
434
+ <MetaTagRow
435
+ label="Meta Keywords"
436
+ value={results.basic.keywords}
437
+ status={results.basic.keywords ? 'success' : 'warning'}
438
+ recommendation="Optional - most search engines don't use this anymore."
439
+ />
440
+ <MetaTagRow
441
+ label="Robots Meta Tag"
442
+ value={results.basic.robots}
443
+ status={results.basic.robots ? 'success' : 'warning'}
444
+ recommendation="Controls how search engines crawl and index your page."
445
+ />
446
+ <MetaTagRow
447
+ label="Canonical URL"
448
+ value={results.basic.canonical}
449
+ status={results.basic.canonical ? 'success' : 'warning'}
450
+ recommendation="Helps prevent duplicate content issues."
451
+ />
452
+ </div>
453
+ </div>
454
+
455
+ {/* Open Graph Tags */}
456
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20">
457
+ <h2 className="text-2xl font-bold text-gray-800 mb-6">Open Graph Tags (Facebook/LinkedIn)</h2>
458
+ <div className="space-y-4">
459
+ <MetaTagRow
460
+ label="OG Title"
461
+ value={results.openGraph.title}
462
+ status={getStatus(results.openGraph.title)}
463
+ recommendation="Should be engaging and optimized for social sharing."
464
+ />
465
+ <MetaTagRow
466
+ label="OG Description"
467
+ value={results.openGraph.description}
468
+ status={getStatus(results.openGraph.description)}
469
+ recommendation="Should be compelling and encourage social shares."
470
+ />
471
+ <MetaTagRow
472
+ label="OG Image"
473
+ value={results.openGraph.image}
474
+ status={getStatus(results.openGraph.image)}
475
+ recommendation="Should be 1200x630px for optimal display on social media."
476
+ />
477
+ <MetaTagRow
478
+ label="OG Type"
479
+ value={results.openGraph.type}
480
+ status={results.openGraph.type ? 'success' : 'warning'}
481
+ recommendation="Specify content type (website, article, etc.)."
482
+ />
483
+ </div>
484
+ </div>
485
+
486
+ {/* Twitter Card Tags */}
487
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20">
488
+ <h2 className="text-2xl font-bold text-gray-800 mb-6">Twitter Card Tags</h2>
489
+ <div className="space-y-4">
490
+ <MetaTagRow
491
+ label="Twitter Card Type"
492
+ value={results.twitter.card}
493
+ status={getStatus(results.twitter.card)}
494
+ recommendation="Use 'summary_large_image' for best visual impact."
495
+ />
496
+ <MetaTagRow
497
+ label="Twitter Title"
498
+ value={results.twitter.title}
499
+ status={results.twitter.title ? 'success' : 'warning'}
500
+ recommendation="Optimized title for Twitter sharing."
501
+ />
502
+ <MetaTagRow
503
+ label="Twitter Description"
504
+ value={results.twitter.description}
505
+ status={results.twitter.description ? 'success' : 'warning'}
506
+ recommendation="Engaging description for Twitter cards."
507
+ />
508
+ <MetaTagRow
509
+ label="Twitter Image"
510
+ value={results.twitter.image}
511
+ status={results.twitter.image ? 'success' : 'warning'}
512
+ recommendation="Should be 1200x600px for summary_large_image cards."
513
+ />
514
+ </div>
515
+ </div>
516
+
517
+ {/* Social Media Previews */}
518
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20">
519
+ <h2 className="text-2xl font-bold text-gray-800 mb-6 flex items-center gap-2">
520
+ <Eye className="w-6 h-6" />
521
+ Social Media Previews
522
+ </h2>
523
+
524
+ <div className="flex gap-2 mb-6 bg-gray-100 p-1 rounded-xl">
525
+ {[
526
+ { key: 'google', label: 'Google', icon: Globe },
527
+ { key: 'facebook', label: 'Facebook', icon: Share2 },
528
+ { key: 'twitter', label: 'Twitter', icon: Share2 }
529
+ ].map(({ key, label, icon: Icon }) => (
530
+ <button
531
+ key={key}
532
+ onClick={() => setActivePreview(key)}
533
+ className={`flex-1 px-4 py-2 rounded-lg font-medium transition-all flex items-center justify-center gap-2 ${
534
+ activePreview === key
535
+ ? 'bg-white text-blue-600 shadow-sm'
536
+ : 'text-gray-600 hover:text-gray-800'
537
+ }`}
538
+ >
539
+ <Icon className="w-4 h-4" />
540
+ {label}
541
+ </button>
542
+ ))}
543
+ </div>
544
+
545
+ {activePreview === 'google' && <GooglePreview data={results} />}
546
+ {activePreview === 'facebook' && <FacebookPreview data={results} />}
547
+ {activePreview === 'twitter' && <TwitterPreview data={results} />}
548
+ </div>
549
+
550
+ {/* Structured Data */}
551
+ {results.structuredData && results.structuredData.length > 0 && (
552
+ <div className="bg-white/80 backdrop-blur-sm rounded-2xl shadow-xl p-6 border border-white/20">
553
+ <h2 className="text-2xl font-bold text-gray-800 mb-6">Structured Data (JSON-LD)</h2>
554
+ <div className="bg-gray-50 rounded-xl p-4">
555
+ <p className="text-green-700 font-medium mb-4 flex items-center gap-2">
556
+ <CheckCircle className="w-5 h-5" />
557
+ Found {results.structuredData.length} structured data object(s)
558
+ </p>
559
+ <pre className="text-sm text-gray-700 overflow-x-auto whitespace-pre-wrap">
560
+ {JSON.stringify(results.structuredData, null, 2)}
561
+ </pre>
562
+ </div>
563
+ </div>
564
+ )}
565
+ </div>
566
+ )}
567
+ </div>
568
  </div>
569
+ );
570
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
571
 
572
+ export default SEOAnalyzer;