Waseem7711 commited on
Commit
7115d4c
·
verified ·
1 Parent(s): 63546b2

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +238 -888
app.py CHANGED
@@ -1,915 +1,265 @@
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; /* Touch-friendly minimum */
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
- .stButton > button:active {
81
- transform: translateY(0);
82
- box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3);
83
- }
84
-
85
- /* Responsive Cards and Containers */
86
- .responsive-card {
87
- background: white;
88
- border-radius: 12px;
89
- padding: 1.5rem;
90
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
91
- margin: 1rem 0;
92
- border: 1px solid #e1e5e9;
93
- }
94
-
95
- /* Mobile-first responsive design */
96
- @media (max-width: 768px) {
97
- .main-header {
98
- padding: 1.5rem 1rem;
99
- margin: 1rem 0;
100
- border-radius: 12px;
101
- }
102
- .main-header h1 {
103
- font-size: 2rem;
104
- line-height: 1.2;
105
- }
106
- .main-header p {
107
- font-size: 1rem;
108
- padding: 0 0.5rem;
109
- }
110
- .responsive-card {
111
- padding: 1rem;
112
- margin: 1rem 0;
113
- border-radius: 8px;
114
- }
115
- .stMetric {
116
- padding: 0.75rem;
117
- margin: 0.5rem 0;
118
- }
119
- .stMetric > div {
120
- font-size: 0.9rem !important;
121
- }
122
- .stMetric > div > div {
123
- text-align: center !important;
124
- }
125
- /* Stack columns on mobile */
126
- .element-container .row-widget {
127
- flex-direction: column !important;
128
- }
129
- .element-container .row-widget > div {
130
- width: 100% !important;
131
- margin-bottom: 1rem !important;
132
- }
133
- }
134
-
135
- @media (max-width: 480px) {
136
- .main-header {
137
- padding: 1rem 0.75rem;
138
- margin: 0.5rem 0;
139
- }
140
- .main-header h1 {
141
- font-size: 1.8rem;
142
- margin-bottom: 0.5rem;
143
- }
144
- .main-header p {
145
- font-size: 0.95rem;
146
- line-height: 1.4;
147
- }
148
- .responsive-card {
149
- padding: 1rem;
150
- border-radius: 10px;
151
- margin: 0.75rem 0;
152
- }
153
- /* Better mobile spacing */
154
- .stButton {
155
- margin: 0.75rem 0 !important;
156
- }
157
- .stTextInput {
158
- margin: 0.5rem 0 !important;
159
- }
160
- .stSelectbox {
161
- margin: 0.5rem 0 !important;
162
- }
163
- /* Improve readability on small screens */
164
- h1, h2, h3 {
165
- line-height: 1.3 !important;
166
- }
167
- p {
168
- line-height: 1.5 !important;
169
- }
170
- }
171
-
172
- /* Responsive Sidebar */
173
- .css-1d391kg {
174
- padding-top: 1rem;
175
- }
176
-
177
- /* Responsive Text Input */
178
- .stTextInput > div > div > input {
179
- font-size: clamp(14px, 3.5vw, 16px);
180
- padding: clamp(12px, 3vw, 16px);
181
- border-radius: 8px;
182
- border: 2px solid #e1e5e9;
183
- min-height: 44px; /* Touch-friendly */
184
- width: 100%;
185
- box-sizing: border-box;
186
- }
187
- .stTextInput > div > div > input:focus {
188
- border-color: #667eea;
189
- outline: none;
190
- box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
191
- }
192
-
193
- /* Responsive Sidebar */
194
- .css-1d391kg {
195
- padding: clamp(1rem, 3vw, 1.5rem);
196
- }
197
-
198
- /* Responsive Columns */
199
- @media (max-width: 768px) {
200
- .row-widget.stHorizontal {
201
- flex-direction: column !important;
202
- }
203
- .row-widget.stHorizontal > div {
204
- width: 100% !important;
205
- margin-bottom: 1rem !important;
206
- }
207
- /* Ensure all columns stack properly */
208
- .stColumns {
209
- flex-direction: column !important;
210
- }
211
- .stColumns > div {
212
- width: 100% !important;
213
- margin-bottom: 1rem !important;
214
- }
215
- }
216
-
217
- /* Responsive Metrics */
218
- .metric-container {
219
- display: flex;
220
- flex-wrap: wrap;
221
- gap: 1rem;
222
- justify-content: space-between;
223
- }
224
-
225
- @media (max-width: 768px) {
226
- .metric-container {
227
- flex-direction: column;
228
- gap: 0.5rem;
229
- }
230
- }
231
-
232
- /* Responsive Preview Cards */
233
- .preview-card {
234
- max-width: 100%;
235
- overflow-x: auto;
236
- margin: 1rem 0;
237
- }
238
-
239
- @media (max-width: 768px) {
240
- .preview-card {
241
- font-size: 0.9rem;
242
- }
243
- .preview-card > div {
244
- padding: 1rem !important;
245
- }
246
- }
247
-
248
- /* Responsive Progress Bar */
249
- .stProgress {
250
- margin: 1rem 0;
251
- }
252
-
253
- /* Responsive Expandables */
254
- .streamlit-expanderHeader {
255
- font-size: clamp(0.9rem, 2vw, 1.1rem);
256
- }
257
-
258
- /* Responsive JSON Display */
259
- .stJson {
260
- font-size: 0.8rem;
261
- overflow-x: auto;
262
- }
263
-
264
- @media (max-width: 768px) {
265
- .stJson {
266
- font-size: 0.7rem;
267
- }
268
- }
269
- </style>
270
- """, unsafe_allow_html=True)
271
-
272
- # Main header
273
- st.markdown("""
274
- <div class="main-header">
275
- <h1>🔍 SEO Metadata Analyzer</h1>
276
- <p>Analyze your website's SEO performance and get actionable insights with visual previews</p>
277
- </div>
278
- """, unsafe_allow_html=True)
279
-
280
- # Center-aligned URL input
281
- st.markdown("""
282
- <div style="text-align: center; max-width: 800px; margin: 0 auto;">
283
- <h1>Analyze and visualize SEO tags tool</h1>
284
- </div>
285
- """, unsafe_allow_html=True)
286
-
287
- col1, col2, col3 = st.columns([1,2,1])
288
- with col2:
289
- domain_input = st.text_input(
290
- "Enter website URL",
291
- placeholder="example.com",
292
- key="domain_input"
293
- )
294
-
295
- if domain_input:
296
- url_input = f"https://{domain_input.strip().replace('https://', '').replace('http://', '')}"
297
- else:
298
- url_input = ""
299
-
300
- analyze_button = st.button("Analyze SEO", type="primary", use_container_width=True)
301
-
302
- if url_input:
303
- normalized_url, error = validate_url(url_input)
304
- if error:
305
- st.error(error)
306
- elif normalized_url:
307
- st.success(f"✅ Ready to analyze: {normalized_url}")
308
-
309
- st.markdown("---")
310
- st.markdown("💡 **Pro Tip:** Try analyzing popular sites like GitHub, Twitter, or LinkedIn to see examples of good SEO!")
311
-
312
- # Main content area
313
- if analyze_button and url_input:
314
- normalized_url, error = validate_url(url_input)
315
-
316
- if error:
317
- st.error(error)
318
- return
319
-
320
- # Progress indicator
321
- progress_bar = st.progress(0)
322
- status_text = st.empty()
323
-
324
  try:
325
- # Initialize analyzer with proper headers and config
326
- status_text.text("🌐 Fetching website data...")
327
- progress_bar.progress(20)
328
 
329
- if not url_input:
330
- st.error("Please enter a URL")
331
- return
332
 
333
- analyzer = SEOAnalyzer()
334
- analysis_result = analyzer.analyze_website(normalized_url)
335
 
336
- if not analysis_result['success']:
337
- st.error(f"❌ Failed to analyze website: {analysis_result['error']}")
338
- return
339
 
340
- progress_bar.progress(60)
341
- status_text.text("🔍 Analyzing SEO metadata...")
342
 
343
- # Generate previews
344
- progress_bar.progress(80)
345
- status_text.text("🎨 Generating previews...")
346
 
347
- preview_gen = PreviewGenerator(analysis_result['metadata'])
 
 
 
 
 
 
 
348
 
349
- progress_bar.progress(100)
350
- status_text.text("✅ Analysis complete!")
351
- time.sleep(0.5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
352
 
353
- # Clear progress indicators
354
- progress_bar.empty()
355
- status_text.empty()
 
 
356
 
357
- # Display results
358
- display_results(analysis_result, preview_gen, normalized_url)
 
 
 
359
 
360
- except Exception as e:
361
- st.error(f"❌ An error occurred during analysis: {str(e)}")
362
- progress_bar.empty()
363
- status_text.empty()
364
-
365
- else:
366
- # Show welcome dashboard when no analysis is running
367
- col1, col2, col3 = st.columns([1, 2, 1])
368
- with col2:
369
- st.markdown("""
370
- <div style="
371
- background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
372
- padding: 2rem;
373
- border-radius: 15px;
374
- margin: 2rem 0;
375
- box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
376
- ">
377
- <h3 style="text-align: center; color: #2c3e50; margin-bottom: 1.5rem;">🎯 How It Works</h3>
378
-
379
- <div style="display: flex; align-items: center; margin-bottom: 1rem;">
380
- <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>
381
- <span><strong>Enter a URL</strong> in the sidebar (e.g., google.com, github.com)</span>
382
- </div>
383
-
384
- <div style="display: flex; align-items: center; margin-bottom: 1rem;">
385
- <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>
386
- <span><strong>Click Analyze SEO</strong> to start the comprehensive analysis</span>
387
- </div>
388
-
389
- <div style="display: flex; align-items: center; margin-bottom: 1.5rem;">
390
- <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>
391
- <span><strong>Review detailed results</strong> with actionable insights</span>
392
- </div>
393
-
394
- <h4 style="color: #2c3e50; margin-bottom: 1rem;">📊 Complete SEO Analysis Includes:</h4>
395
- <ul style="color: #34495e; line-height: 1.6;">
396
- <li><strong>SEO Score & Status</strong> - Overall website rating</li>
397
- <li><strong>Meta Tags Analysis</strong> - Title, description, keywords</li>
398
- <li><strong>Social Media Previews</strong> - Facebook, Twitter cards</li>
399
- <li><strong>Google Search Preview</strong> - How your site appears in search</li>
400
- <li><strong>Technical SEO Check</strong> - Viewport, canonical URLs, robots</li>
401
- <li><strong>Structured Data</strong> - JSON-LD schema markup</li>
402
- <li><strong>Priority Recommendations</strong> - Step-by-step improvement guide</li>
403
- </ul>
404
- </div>
405
- """, unsafe_allow_html=True)
406
-
407
- # Example websites to try
408
- st.markdown("### 🌟 Try These Popular Websites:")
409
-
410
- example_col1, example_col2, example_col3 = st.columns(3)
411
 
412
- with example_col1:
413
- if st.button("📚 GitHub", use_container_width=True):
414
- st.session_state.example_url = "github.com"
415
- st.rerun()
416
-
417
- with example_col2:
418
- if st.button("🔍 Google", use_container_width=True):
419
- st.session_state.example_url = "google.com"
420
- st.rerun()
421
-
422
- with example_col3:
423
- if st.button("💼 LinkedIn", use_container_width=True):
424
- st.session_state.example_url = "linkedin.com"
425
- st.rerun()
426
 
427
- # Handle example URL selection
428
- if hasattr(st.session_state, 'example_url'):
429
- st.info(f"💡 Example selected: {st.session_state.example_url} - Enter this in the sidebar and click Analyze!")
430
-
431
- def display_results(analysis_result, preview_gen, url):
432
- """Display the complete analysis results"""
433
- metadata = analysis_result['metadata']
434
- seo_score = analysis_result['seo_score']
435
- recommendations = analysis_result['recommendations']
436
-
437
- # Overall SEO Status Header
438
- st.header("🎯 SEO Analysis Report")
439
-
440
- # Quick Visual Summary Dashboard
441
- st.markdown("### 📊 At-a-Glance Summary")
442
-
443
- # Calculate quick stats
444
- total_issues = len([r for r in recommendations if r['type'] == 'error'])
445
- warnings = len([r for r in recommendations if r['type'] == 'warning'])
446
- good_elements = 0
447
-
448
- # Count good elements
449
- if metadata.get('title') and 30 <= len(metadata.get('title', '')) <= 60:
450
- good_elements += 1
451
- if metadata.get('description') and 120 <= len(metadata.get('description', '')) <= 160:
452
- good_elements += 1
453
- if metadata.get('og:title') and metadata.get('og:description'):
454
- good_elements += 1
455
- if metadata.get('viewport'):
456
- good_elements += 1
457
-
458
- # Create summary cards
459
- summary_col1, summary_col2, summary_col3, summary_col4 = st.columns(4)
460
-
461
- with summary_col1:
462
- st.markdown(f"""
463
- <div style="
464
- background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
465
- color: white;
466
- padding: 1.5rem;
467
- border-radius: 12px;
468
- text-align: center;
469
- box-shadow: 0 4px 15px rgba(40, 167, 69, 0.3);
470
- ">
471
- <div style="font-size: 2.5rem; margin-bottom: 0.5rem;">✅</div>
472
- <div style="font-size: 2rem; font-weight: bold;">{good_elements}</div>
473
- <div style="font-size: 0.9rem; opacity: 0.9;">Good Elements</div>
474
- </div>
475
- """, unsafe_allow_html=True)
476
-
477
- with summary_col2:
478
- st.markdown(f"""
479
- <div style="
480
- background: linear-gradient(135deg, #ffc107 0%, #ffca28 100%);
481
- color: white;
482
- padding: 1.5rem;
483
- border-radius: 12px;
484
- text-align: center;
485
- box-shadow: 0 4px 15px rgba(255, 193, 7, 0.3);
486
- ">
487
- <div style="font-size: 2.5rem; margin-bottom: 0.5rem;">⚠️</div>
488
- <div style="font-size: 2rem; font-weight: bold;">{warnings}</div>
489
- <div style="font-size: 0.9rem; opacity: 0.9;">Improvements</div>
490
- </div>
491
- """, unsafe_allow_html=True)
492
-
493
- with summary_col3:
494
- st.markdown(f"""
495
- <div style="
496
- background: linear-gradient(135deg, #dc3545 0%, #e74c3c 100%);
497
- color: white;
498
- padding: 1.5rem;
499
- border-radius: 12px;
500
- text-align: center;
501
- box-shadow: 0 4px 15px rgba(220, 53, 69, 0.3);
502
- ">
503
- <div style="font-size: 2.5rem; margin-bottom: 0.5rem;">❌</div>
504
- <div style="font-size: 2rem; font-weight: bold;">{total_issues}</div>
505
- <div style="font-size: 0.9rem; opacity: 0.9;">Critical Issues</div>
506
- </div>
507
- """, unsafe_allow_html=True)
508
 
509
- with summary_col4:
510
- # Overall readiness percentage
511
- total_checks = 8 # Total possible good elements
512
- readiness = round((good_elements / total_checks) * 100)
513
- readiness_color = "#28a745" if readiness >= 70 else "#ffc107" if readiness >= 40 else "#dc3545"
514
 
515
- st.markdown(f"""
516
- <div style="
517
- background: linear-gradient(135deg, {readiness_color} 0%, {readiness_color}dd 100%);
518
- color: white;
519
- padding: 1.5rem;
520
- border-radius: 12px;
521
- text-align: center;
522
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
523
- ">
524
- <div style="font-size: 2.5rem; margin-bottom: 0.5rem;">🚀</div>
525
- <div style="font-size: 2rem; font-weight: bold;">{readiness}%</div>
526
- <div style="font-size: 0.9rem; opacity: 0.9;">SEO Ready</div>
527
- </div>
528
- """, unsafe_allow_html=True)
529
-
530
- # Overall Status Card
531
- if seo_score >= 80:
532
- status_color = "#28a745"
533
- status_text = "EXCELLENT"
534
- status_icon = "🟢"
535
- status_message = "Your website has excellent SEO optimization!"
536
- elif seo_score >= 60:
537
- status_color = "#ffc107"
538
- status_text = "GOOD"
539
- status_icon = "🟡"
540
- status_message = "Your website has good SEO with room for improvement."
541
- else:
542
- status_color = "#dc3545"
543
- status_text = "NEEDS IMPROVEMENT"
544
- status_icon = "🔴"
545
- status_message = "Your website needs significant SEO improvements."
546
-
547
- # Main Status Display - Mobile Optimized
548
- st.markdown(f"""
549
- <div style="
550
- background: linear-gradient(135deg, {status_color}15, {status_color}05);
551
- border: 2px solid {status_color};
552
- border-radius: 15px;
553
- padding: clamp(15px, 5vw, 25px);
554
- margin: 20px auto;
555
- text-align: center;
556
- max-width: 100%;
557
- display: flex;
558
- flex-direction: column;
559
- align-items: center;
560
- justify-content: center;
561
- ">
562
- <h1 style="
563
- margin: 0;
564
- color: {status_color};
565
- font-size: clamp(2.5rem, 8vw, 4rem);
566
- font-weight: 800;
567
- line-height: 1;
568
- ">{seo_score}</h1>
569
- <h2 style="
570
- margin: clamp(8px, 2vw, 15px) 0;
571
- color: {status_color};
572
- font-size: clamp(1.2rem, 4vw, 1.8rem);
573
- font-weight: 600;
574
- ">{status_icon} {status_text}</h2>
575
- <p style="
576
- margin: 0;
577
- font-size: clamp(1rem, 3vw, 1.2rem);
578
- color: #666;
579
- line-height: 1.4;
580
- text-align: center;
581
- max-width: 90%;
582
- ">{status_message}</p>
583
- </div>
584
- """, unsafe_allow_html=True)
585
-
586
- # Visual SEO Category Summaries
587
- st.subheader("📊 SEO Category Analysis")
588
 
589
- # Calculate category scores
590
- def calculate_category_score(category_name, metadata):
591
- if category_name == "Basic Meta Tags":
592
- score = 0
593
- title = metadata.get('title', '')
594
- description = metadata.get('description', '')
595
-
596
- if title:
597
- score += 40
598
- if 30 <= len(title) <= 60:
599
- score += 20
600
- if description:
601
- score += 40
602
- if 120 <= len(description) <= 160:
603
- score += 20
604
- if metadata.get('keywords'):
605
  score += 10
606
- if metadata.get('robots'):
 
 
 
 
 
607
  score += 10
608
- return min(score, 100)
609
-
610
- elif category_name == "Social Media":
611
- score = 0
612
- # Open Graph
613
- if metadata.get('og:title'): score += 20
614
- if metadata.get('og:description'): score += 20
615
- if metadata.get('og:image'): score += 20
616
- # Twitter
617
- if metadata.get('twitter:card'): score += 15
618
- if metadata.get('twitter:title'): score += 15
619
- if metadata.get('twitter:description'): score += 10
620
- return min(score, 100)
621
-
622
- elif category_name == "Technical SEO":
623
- score = 0
624
- if metadata.get('viewport'): score += 25
625
- if metadata.get('charset'): score += 25
626
- if metadata.get('link-canonical'): score += 25
627
- if metadata.get('robots') and 'noindex' not in metadata.get('robots', '').lower(): score += 25
628
- return min(score, 100)
629
 
630
- return 0
631
-
632
- # Create visual category cards
633
- categories = [
634
- ("Basic Meta Tags", "📝", "Title, Description, Keywords"),
635
- ("Social Media", "📱", "Facebook, Twitter, Open Graph"),
636
- ("Technical SEO", "⚙️", "Viewport, Charset, Canonical"),
637
- ("Performance", "⚡", "Loading, Structure, Schema")
638
- ]
639
-
640
- cols = st.columns(4)
641
-
642
- for i, (category, icon, description) in enumerate(categories):
643
- with cols[i]:
644
- if category == "Performance":
645
- # For performance, use structured data presence
646
- score = 100 if analysis_result.get('structured_data') else 50
647
- else:
648
- score = calculate_category_score(category, metadata)
649
-
650
- # Determine color and status
651
- if score >= 80:
652
- color = "#28a745"
653
- status = "Excellent"
654
- bg_color = "#d4edda"
655
- elif score >= 60:
656
- color = "#ffc107"
657
- status = "Good"
658
- bg_color = "#fff3cd"
659
- else:
660
- color = "#dc3545"
661
- status = "Needs Work"
662
- bg_color = "#f8d7da"
663
-
664
- st.markdown(f"""
665
- <div style="
666
- background: {bg_color};
667
- border: 2px solid {color};
668
- border-radius: 12px;
669
- padding: 1rem;
670
- text-align: center;
671
- margin: 0.5rem 0;
672
- min-height: 140px;
673
- display: flex;
674
- flex-direction: column;
675
- justify-content: center;
676
- ">
677
- <div style="font-size: 2rem; margin-bottom: 0.5rem;">{icon}</div>
678
- <div style="font-weight: bold; color: {color}; font-size: 1.1rem; margin-bottom: 0.3rem;">
679
- {category}
680
- </div>
681
- <div style="font-size: 2rem; font-weight: bold; color: {color}; margin: 0.3rem 0;">
682
- {score}%
683
- </div>
684
- <div style="font-size: 0.9rem; color: {color}; font-weight: 600;">
685
- {status}
686
- </div>
687
- <div style="font-size: 0.8rem; color: #666; margin-top: 0.3rem;">
688
- {description}
689
- </div>
690
- </div>
691
- """, unsafe_allow_html=True)
692
-
693
- # Visual SEO Progress Chart
694
- st.subheader("📈 SEO Progress Overview")
695
-
696
- # Calculate individual scores for visualization
697
- basic_score = calculate_category_score("Basic Meta Tags", metadata)
698
- social_score = calculate_category_score("Social Media", metadata)
699
- technical_score = calculate_category_score("Technical SEO", metadata)
700
- performance_score = 100 if analysis_result.get('structured_data') else 50
701
-
702
- # Create a visual progress bar for each category
703
- progress_data = [
704
- ("📝 Basic Meta Tags", basic_score, "#4CAF50"),
705
- ("📱 Social Media", social_score, "#2196F3"),
706
- ("⚙️ Technical SEO", technical_score, "#FF9800"),
707
- ("⚡ Performance", performance_score, "#9C27B0")
708
- ]
709
-
710
- for name, score, color in progress_data:
711
- col1, col2, col3 = st.columns([3, 1, 1])
712
- with col1:
713
- # Create visual progress bar
714
- progress_width = score
715
- st.markdown(f"""
716
- <div style="
717
- background: #f0f0f0;
718
- border-radius: 10px;
719
- height: 25px;
720
- position: relative;
721
- overflow: hidden;
722
- margin: 8px 0;
723
- ">
724
- <div style="
725
- background: linear-gradient(90deg, {color} 0%, {color}dd 100%);
726
- height: 100%;
727
- width: {progress_width}%;
728
- border-radius: 10px;
729
- transition: width 0.3s ease;
730
- display: flex;
731
- align-items: center;
732
- padding-left: 10px;
733
- color: white;
734
- font-weight: bold;
735
- font-size: 12px;
736
- ">
737
- {name}
738
- </div>
739
- </div>
740
- """, unsafe_allow_html=True)
741
- with col2:
742
- st.markdown(f"**{score}%**")
743
- with col3:
744
- if score >= 80:
745
- st.markdown("🟢 Great")
746
- elif score >= 60:
747
- st.markdown("🟡 Good")
748
- else:
749
- st.markdown("🔴 Fix")
750
-
751
- # Beginner-Friendly SEO Checklist
752
- st.subheader("✅ SEO Checklist for Beginners")
753
- st.markdown("*Here's what each element means and why it matters:*")
754
-
755
- checklist_items = [
756
- {
757
- "name": "Page Title",
758
- "value": metadata.get('title'),
759
- "description": "The title that appears in search results and browser tabs",
760
- "why": "Helps search engines understand your page content",
761
- "ideal": "30-60 characters, include main keywords"
762
- },
763
- {
764
- "name": "Meta Description",
765
- "value": metadata.get('description'),
766
- "description": "The summary text shown under your title in search results",
767
- "why": "Encourages people to click on your website",
768
- "ideal": "120-160 characters, compelling and descriptive"
769
- },
770
- {
771
- "name": "Facebook Sharing (Open Graph)",
772
- "value": metadata.get('og:title') or metadata.get('og:description') or metadata.get('og:image'),
773
- "description": "How your page looks when shared on Facebook",
774
- "why": "Makes your content look professional when shared",
775
- "ideal": "Title, description, and image (1200x630px)"
776
- },
777
- {
778
- "name": "Twitter Sharing",
779
- "value": metadata.get('twitter:card') or metadata.get('twitter:title'),
780
- "description": "How your page appears when shared on Twitter",
781
- "why": "Increases engagement on social media",
782
- "ideal": "Card type, title, and description"
783
- },
784
- {
785
- "name": "Mobile-Friendly",
786
- "value": metadata.get('viewport'),
787
- "description": "Makes your website work well on phones and tablets",
788
- "why": "Most people browse on mobile devices",
789
- "ideal": "Should be present for all websites"
790
- }
791
- ]
792
-
793
- for item in checklist_items:
794
- with st.expander(f"{'✅' if item['value'] else '❌'} {item['name']}", expanded=False):
795
- col1, col2 = st.columns([2, 1])
796
- with col1:
797
- st.markdown(f"**What it is:** {item['description']}")
798
- st.markdown(f"**Why it matters:** {item['why']}")
799
- st.markdown(f"**Best practice:** {item['ideal']}")
800
-
801
- if item['value']:
802
- if item['name'] in ["Page Title", "Meta Description"]:
803
- char_count = len(str(item['value']))
804
- st.success(f"✅ Present ({char_count} characters)")
805
- if item['name'] == "Page Title" and not (30 <= char_count <= 60):
806
- st.warning("⚠️ Consider adjusting length to 30-60 characters")
807
- elif item['name'] == "Meta Description" and not (120 <= char_count <= 160):
808
- st.warning("⚠️ Consider adjusting length to 120-160 characters")
809
- else:
810
- st.success("✅ Implemented correctly")
811
- else:
812
- st.error("❌ Missing - Consider adding this to improve SEO")
813
-
814
- with col2:
815
- if item['value']:
816
- st.markdown("**Status:** 🟢 Good")
817
- else:
818
- st.markdown("**Status:** 🔴 Needs attention")
819
-
820
- # Recommendations
821
- if recommendations:
822
- st.subheader("🎯 Priority Recommendations")
823
 
824
- # Group recommendations by priority
825
- errors = [r for r in recommendations if r['type'] == 'error']
826
- warnings = [r for r in recommendations if r['type'] == 'warning']
827
- info = [r for r in recommendations if r['type'] == 'info']
 
 
828
 
829
- if errors:
830
- st.markdown("**🚨 Critical Issues (Fix First):**")
831
- for rec in errors:
832
- st.error(f"❌ {rec['message']}")
833
 
834
- if warnings:
835
- st.markdown("**⚠️ Important Improvements:**")
836
- for rec in warnings:
837
- st.warning(f"⚠️ {rec['message']}")
 
 
838
 
839
- if info:
840
- st.markdown("**💡 Suggested Enhancements:**")
841
- for rec in info:
842
- st.info(f"💡 {rec['message']}")
843
-
844
- # Previews
845
- st.header("👀 Search & Social Media Previews")
846
-
847
- # Google Search Preview
848
- st.subheader("🔍 Google Search Result Preview")
849
- google_preview = preview_gen.generate_google_preview(url)
850
- st.markdown(google_preview, unsafe_allow_html=True)
851
-
852
- # Social Media Previews - Responsive Layout
853
- # On mobile, these will stack vertically automatically
854
- col1, col2 = st.columns([1, 1])
855
-
856
- with col1:
857
- st.subheader("📘 Facebook Preview")
858
- facebook_preview = preview_gen.generate_facebook_preview(url)
859
- st.markdown(facebook_preview, unsafe_allow_html=True)
860
-
861
- with col2:
862
- st.subheader("🐦 Twitter Preview")
863
- twitter_preview = preview_gen.generate_twitter_preview(url)
864
- st.markdown(twitter_preview, unsafe_allow_html=True)
865
-
866
- # Detailed Metadata
867
- st.header("🔍 Detailed Metadata Analysis")
868
-
869
- # Basic Meta Tags
870
- with st.expander("📝 Basic Meta Tags", expanded=True):
871
- meta_data = {
872
- "Title": metadata.get('title', 'Not found'),
873
- "Description": metadata.get('description', 'Not found'),
874
- "Keywords": metadata.get('keywords', 'Not found'),
875
- "Robots": metadata.get('robots', 'Not found'),
876
- "Viewport": metadata.get('viewport', 'Not found'),
877
- "Charset": metadata.get('charset', 'Not found')
878
- }
879
 
880
- for key, value in meta_data.items():
881
- if value != 'Not found':
882
- st.success(f"**{key}**: {value}")
883
- else:
884
- st.error(f"**{key}**: {value}")
885
-
886
- # Open Graph Tags
887
- with st.expander("📱 Open Graph Tags"):
888
- og_tags = {k: v for k, v in metadata.items() if k.startswith('og:')}
889
- if og_tags:
890
- for key, value in og_tags.items():
891
- st.info(f"**{key}**: {value}")
892
- else:
893
- st.warning("No Open Graph tags found")
894
-
895
- # Twitter Card Tags
896
- with st.expander("🐦 Twitter Card Tags"):
897
- twitter_tags = {k: v for k, v in metadata.items() if k.startswith('twitter:')}
898
- if twitter_tags:
899
- for key, value in twitter_tags.items():
900
- st.info(f"**{key}**: {value}")
901
- else:
902
- st.warning("No Twitter Card tags found")
903
 
904
- # JSON-LD Structured Data
905
- if 'structured_data' in analysis_result and analysis_result['structured_data']:
906
- with st.expander("🏗️ Structured Data (JSON-LD)"):
907
- st.json(analysis_result['structured_data'])
908
-
909
- # Raw HTML Head
910
- with st.expander("🔧 Raw HTML Head (First 50 lines)"):
911
- if 'html_head' in analysis_result:
912
- st.code(analysis_result['html_head'][:2000] + "..." if len(analysis_result['html_head']) > 2000 else analysis_result['html_head'], language='html')
913
-
914
- if __name__ == "__main__":
915
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import requests
2
+ from bs4 import BeautifulSoup
3
+ import json
4
+ import re
5
+ from urllib.parse import urljoin, urlparse
6
 
7
+ class SEOAnalyzer:
8
+ def __init__(self):
9
+ self.session = requests.Session()
10
+ self.session.headers.update({
11
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36'
12
+ })
13
+
14
+ def analyze_website(self, url):
15
+ """Main analysis function"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
  try:
17
+ # Fetch the webpage
18
+ response = self.session.get(url, timeout=10, allow_redirects=True)
19
+ response.raise_for_status()
20
 
21
+ # Parse HTML
22
+ soup = BeautifulSoup(response.text, 'html.parser')
 
23
 
24
+ # Extract metadata
25
+ metadata = self.extract_metadata(soup)
26
 
27
+ # Extract structured data
28
+ structured_data = self.extract_structured_data(soup)
 
29
 
30
+ # Calculate SEO score
31
+ seo_score = self.calculate_seo_score(metadata)
32
 
33
+ # Generate recommendations
34
+ recommendations = self.generate_recommendations(metadata, url)
 
35
 
36
+ return {
37
+ 'success': True,
38
+ 'metadata': metadata,
39
+ 'structured_data': structured_data,
40
+ 'seo_score': seo_score,
41
+ 'recommendations': recommendations,
42
+ 'html_head': str(soup.head) if soup.head else "Head section not found"
43
+ }
44
 
45
+ except requests.exceptions.RequestException as e:
46
+ return {
47
+ 'success': False,
48
+ 'error': f"Failed to fetch website: {str(e)}"
49
+ }
50
+ except Exception as e:
51
+ return {
52
+ 'success': False,
53
+ 'error': f"Analysis error: {str(e)}"
54
+ }
55
+
56
+ def extract_metadata(self, soup):
57
+ """Extract all relevant metadata from the page"""
58
+ metadata = {}
59
+
60
+ # Basic meta tags
61
+ if soup.title:
62
+ metadata['title'] = soup.title.string.strip() if soup.title.string else ""
63
+
64
+ # Meta tags
65
+ meta_tags = soup.find_all('meta')
66
+ for tag in meta_tags:
67
+ # Standard meta tags
68
+ if tag.get('name'):
69
+ name = tag.get('name').lower()
70
+ content = tag.get('content', '')
71
+ metadata[name] = content
72
 
73
+ # Property meta tags (Open Graph, etc.)
74
+ elif tag.get('property'):
75
+ prop = tag.get('property').lower()
76
+ content = tag.get('content', '')
77
+ metadata[prop] = content
78
 
79
+ # HTTP-equiv meta tags
80
+ elif tag.get('http-equiv'):
81
+ equiv = tag.get('http-equiv').lower()
82
+ content = tag.get('content', '')
83
+ metadata[f'http-equiv-{equiv}'] = content
84
 
85
+ # Charset
86
+ elif tag.get('charset'):
87
+ metadata['charset'] = tag.get('charset')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
 
89
+ # Link tags (canonical, etc.)
90
+ link_tags = soup.find_all('link')
91
+ for tag in link_tags:
92
+ rel = tag.get('rel')
93
+ if rel:
94
+ rel_str = ' '.join(rel) if isinstance(rel, list) else rel
95
+ if rel_str in ['canonical', 'alternate', 'prev', 'next']:
96
+ metadata[f'link-{rel_str}'] = tag.get('href', '')
 
 
 
 
 
 
97
 
98
+ return metadata
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
 
100
+ def extract_structured_data(self, soup):
101
+ """Extract JSON-LD structured data"""
102
+ structured_data = []
 
 
103
 
104
+ scripts = soup.find_all('script', type='application/ld+json')
105
+ for script in scripts:
106
+ try:
107
+ if script.string:
108
+ data = json.loads(script.string.strip())
109
+ structured_data.append(data)
110
+ except json.JSONDecodeError:
111
+ continue
112
+
113
+ return structured_data
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
 
115
+ def calculate_seo_score(self, metadata):
116
+ """Calculate SEO score based on best practices"""
117
+ score = 0
118
+ max_score = 100
119
+
120
+ # Title tag (20 points)
121
+ title = metadata.get('title', '')
122
+ if title:
123
+ score += 10
124
+ if 30 <= len(title) <= 60:
 
 
 
 
 
 
125
  score += 10
126
+
127
+ # Meta description (20 points)
128
+ description = metadata.get('description', '')
129
+ if description:
130
+ score += 10
131
+ if 120 <= len(description) <= 160:
132
  score += 10
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
133
 
134
+ # Open Graph tags (20 points)
135
+ og_title = metadata.get('og:title', '')
136
+ og_description = metadata.get('og:description', '')
137
+ og_image = metadata.get('og:image', '')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
 
139
+ if og_title:
140
+ score += 7
141
+ if og_description:
142
+ score += 7
143
+ if og_image:
144
+ score += 6
145
 
146
+ # Twitter Card (15 points)
147
+ twitter_card = metadata.get('twitter:card', '')
148
+ twitter_title = metadata.get('twitter:title', '')
149
+ twitter_description = metadata.get('twitter:description', '')
150
 
151
+ if twitter_card:
152
+ score += 5
153
+ if twitter_title:
154
+ score += 5
155
+ if twitter_description:
156
+ score += 5
157
 
158
+ # Technical SEO (25 points)
159
+ if metadata.get('viewport'):
160
+ score += 5
161
+ if metadata.get('charset'):
162
+ score += 5
163
+ if metadata.get('robots'):
164
+ score += 5
165
+ if metadata.get('link-canonical'):
166
+ score += 5
167
+ if not metadata.get('robots') or 'noindex' not in metadata.get('robots', '').lower():
168
+ score += 5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
 
170
+ return min(score, max_score)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
 
172
+ def generate_recommendations(self, metadata, url):
173
+ """Generate actionable SEO recommendations"""
174
+ recommendations = []
175
+
176
+ # Title tag recommendations
177
+ title = metadata.get('title', '')
178
+ if not title:
179
+ recommendations.append({
180
+ 'type': 'error',
181
+ 'message': 'Missing title tag. Add a descriptive title between 30-60 characters.'
182
+ })
183
+ elif len(title) < 30:
184
+ recommendations.append({
185
+ 'type': 'warning',
186
+ 'message': f'Title tag is too short ({len(title)} chars). Aim for 30-60 characters.'
187
+ })
188
+ elif len(title) > 60:
189
+ recommendations.append({
190
+ 'type': 'warning',
191
+ 'message': f'Title tag is too long ({len(title)} chars). Keep it under 60 characters to avoid truncation.'
192
+ })
193
+
194
+ # Meta description recommendations
195
+ description = metadata.get('description', '')
196
+ if not description:
197
+ recommendations.append({
198
+ 'type': 'error',
199
+ 'message': 'Missing meta description. Add a compelling description between 120-160 characters.'
200
+ })
201
+ elif len(description) < 120:
202
+ recommendations.append({
203
+ 'type': 'warning',
204
+ 'message': f'Meta description is too short ({len(description)} chars). Aim for 120-160 characters.'
205
+ })
206
+ elif len(description) > 160:
207
+ recommendations.append({
208
+ 'type': 'warning',
209
+ 'message': f'Meta description is too long ({len(description)} chars). Keep it under 160 characters.'
210
+ })
211
+
212
+ # Open Graph recommendations
213
+ if not metadata.get('og:title'):
214
+ recommendations.append({
215
+ 'type': 'warning',
216
+ 'message': 'Missing Open Graph title. Add og:title for better social media sharing.'
217
+ })
218
+
219
+ if not metadata.get('og:description'):
220
+ recommendations.append({
221
+ 'type': 'warning',
222
+ 'message': 'Missing Open Graph description. Add og:description for social media previews.'
223
+ })
224
+
225
+ if not metadata.get('og:image'):
226
+ recommendations.append({
227
+ 'type': 'warning',
228
+ 'message': 'Missing Open Graph image. Add og:image (1200x630px recommended) for social sharing.'
229
+ })
230
+
231
+ # Twitter Card recommendations
232
+ if not metadata.get('twitter:card'):
233
+ recommendations.append({
234
+ 'type': 'info',
235
+ 'message': 'Consider adding Twitter Card meta tags for better Twitter sharing experience.'
236
+ })
237
+
238
+ # Technical SEO recommendations
239
+ if not metadata.get('viewport'):
240
+ recommendations.append({
241
+ 'type': 'error',
242
+ 'message': 'Missing viewport meta tag. Add <meta name="viewport" content="width=device-width, initial-scale=1"> for mobile optimization.'
243
+ })
244
+
245
+ if not metadata.get('charset'):
246
+ recommendations.append({
247
+ 'type': 'warning',
248
+ 'message': 'Missing charset declaration. Add <meta charset="UTF-8"> in the head section.'
249
+ })
250
+
251
+ if not metadata.get('link-canonical'):
252
+ recommendations.append({
253
+ 'type': 'info',
254
+ 'message': 'Consider adding a canonical URL to prevent duplicate content issues.'
255
+ })
256
+
257
+ # Robots meta tag
258
+ robots = metadata.get('robots', '')
259
+ if 'noindex' in robots.lower():
260
+ recommendations.append({
261
+ 'type': 'warning',
262
+ 'message': 'Page is set to noindex. Remove this if you want the page to be indexed by search engines.'
263
+ })
264
+
265
+ return recommendations