File size: 13,156 Bytes
9bf19c4
8913f77
c0caea8
96dcaeb
 
 
795900a
8913f77
 
 
 
 
 
 
 
3b6511b
9bf19c4
c0caea8
 
5f0cfa7
 
c0caea8
96dcaeb
5f0cfa7
8913f77
 
 
c0caea8
bb97ea5
8913f77
c0caea8
9bf19c4
8913f77
96dcaeb
5f0cfa7
 
96dcaeb
 
5f0cfa7
8913f77
 
 
 
 
96dcaeb
9bf19c4
96dcaeb
 
795900a
 
 
 
 
 
 
 
 
 
 
 
9bf19c4
795900a
 
 
 
 
 
 
 
9bf19c4
795900a
 
 
 
 
 
 
9bf19c4
795900a
 
 
 
 
9bf19c4
795900a
 
 
 
 
 
9bf19c4
795900a
 
 
 
9bf19c4
795900a
9bf19c4
795900a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96dcaeb
 
 
 
 
 
 
 
 
 
c0caea8
 
96dcaeb
c0caea8
 
96dcaeb
c0caea8
9bf19c4
96dcaeb
c0caea8
9bf19c4
795900a
96dcaeb
 
 
 
 
9bf19c4
795900a
 
c0caea8
9bf19c4
c0caea8
 
9bf19c4
c0caea8
 
9bf19c4
8913f77
 
 
 
 
 
 
 
 
 
 
 
 
795900a
9bf19c4
795900a
 
 
 
 
 
 
 
 
 
9bf19c4
795900a
5f0cfa7
9bf19c4
795900a
 
 
 
 
 
 
 
 
5f0cfa7
9bf19c4
5f0cfa7
 
 
 
9bf19c4
c0caea8
96dcaeb
 
 
795900a
9bf19c4
795900a
 
 
 
 
 
 
 
 
 
 
 
 
 
9bf19c4
795900a
 
 
96dcaeb
 
 
5f0cfa7
 
 
96dcaeb
c0caea8
9bf19c4
c0caea8
 
 
 
 
5f0cfa7
 
 
96dcaeb
c0caea8
 
9bf19c4
96dcaeb
 
 
 
 
5f0cfa7
 
 
96dcaeb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5bd6119
8913f77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c0caea8
96dcaeb
135072e
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363

from flask import Flask, render_template, request, jsonify, send_file, redirect, url_for, session
import validators
import os
import tempfile
import uuid
from urllib.parse import urlparse
from typing import Dict, Any, List

# Load environment variables from .env file
try:
    from dotenv import load_dotenv
    load_dotenv()
except ImportError:
    print("python-dotenv not installed. Using system environment variables only.")


from modules.technical_seo import TechnicalSEOModule
from modules.content_audit import ContentAuditModule
from modules.keywords import KeywordsModule
from modules.backlinks import BacklinksModule
from report_generator import ReportGenerator
from simple_pdf_generator import SimplePDFGenerator
from llm_recommendations import LLMRecommendations
from gsc_client import GSCClient
from utils import safe_pct
from benchmarks import BENCHMARKS, badge

app = Flask(__name__, static_folder='static')
app.secret_key = os.getenv('FLASK_SECRET_KEY', 'seo_report_generator_2024')


technical_module = TechnicalSEOModule(api_key=os.getenv('GOOGLE_API_KEY'))
content_module = ContentAuditModule()
keywords_module = KeywordsModule()
backlinks_module = BacklinksModule()
report_gen = ReportGenerator()
pdf_gen = SimplePDFGenerator()
llm_recommendations = LLMRecommendations()
try:
    gsc_client = GSCClient()
except ImportError as e:
    print(f"GSC client not available: {e}")
    gsc_client = None


reports_store = {}

def _transform_keywords_data(new_data: Dict[str, Any]) -> Dict[str, Any]:
    if not new_data or new_data.get('placeholder'):
        return {
            'placeholder': True,
            'message': 'No keyword data available',
            'total_keywords': 0,
            'position_distribution': {'top_3': 0, 'top_10': 0, 'top_50': 0, 'beyond_50': 0},
            'best_keywords': [],
            'opportunity_keywords': [],
            'data_source': 'Analysis failed'
        }
    

    totals = new_data.get('totals', {})
    distribution = new_data.get('distribution', {})
    movement = new_data.get('movement', {})
    best_keywords = new_data.get('best_keywords', [])
    declining_keywords = new_data.get('declining_keywords', [])
    opportunities = new_data.get('opportunities', [])
    data_sources = new_data.get('data_sources', {})
    

    pos_dist = {
        'top_3': distribution.get('top3', 0),
        'top_10': distribution.get('top10', 0),
        'top_50': distribution.get('top50', 0),
        'beyond_50': totals.get('keywords', 0) - distribution.get('top50', 0)
    }
    

    transformed_best_keywords = []
    for kw in best_keywords:
        transformed_best_keywords.append({
            'keyword': kw.get('keyword', ''),
            'position': kw.get('rank', 0),
            'clicks': 0,
            'impressions': kw.get('volume', 0),
            'url': kw.get('url', ''),
            'estimated_traffic': kw.get('estimated_traffic', 0),
            'trend': kw.get('trend', 'stable')
        })
    

    transformed_opportunities = []
    for opp in opportunities:
        transformed_opportunities.append({
            'keyword': opp.get('keyword', ''),
            'position': 0,
            'impressions': opp.get('volume', 0),
            'ctr': 0,
            'competitor_rank': opp.get('competitor_rank', 0),
            'priority_score': opp.get('priority_score', 0),
            'competitor_domain': opp.get('competitor_domain', '')
        })
    
    return {
        'total_keywords': totals.get('keywords', 0),
        'estimated_traffic': totals.get('estimated_traffic', 0),
        'position_distribution': pos_dist,
        'movement': movement,
        'best_keywords': transformed_best_keywords,
        'declining_keywords': declining_keywords,
        'opportunity_keywords': transformed_opportunities,
        'competitor_summary': new_data.get('competitor_summary', []),
        'data_source': f"{data_sources.get('positions', 'Unknown')} + {data_sources.get('volume', 'Unknown')}",
        'enrichment_rate': data_sources.get('enrichment_rate', 0),
        'meta': new_data.get('meta', {}),
        'placeholder': False
    }

@app.route('/')
def index():
    return render_template('index.html')

@app.route('/generate', methods=['POST'])
def generate_report():
    try:
        data = request.json
        url = data.get('url', '').strip()
        competitors = data.get('competitors', [])
        
        if not url:
            return jsonify({'error': 'Website URL is required'}), 400
            
        if not validators.url(url):
            return jsonify({'error': 'Please enter a valid URL'}), 400
        

        report_id = str(uuid.uuid4())
        

        competitor_domains = []
        competitor_list = []
        for comp in competitors:
            comp = comp.strip()
            if comp and validators.url(comp):
                competitor_list.append(comp)

                domain = urlparse(comp).netloc.replace('www.', '')
                competitor_domains.append(domain)
        

        technical_data = technical_module.analyze(url)
        

        content_data = content_module.analyze(url)
        

        # Check if GSC should be used
        use_gsc = False
        if gsc_client and 'gsc_tokens' in session and gsc_client.property_url:
            domain = urlparse(url).netloc.replace('www.', '')
            property_domain = urlparse(gsc_client.property_url).netloc.replace('www.', '')
            if domain == property_domain:
                use_gsc = True
        
        # Analyze keywords
        if use_gsc:
            keywords_result = app._analyze_with_gsc(url, competitor_domains)
        else:
            keywords_result = keywords_module.analyze(url, competitor_domains=competitor_domains)
        if not keywords_result.success:

            keywords_data = {
                'placeholder': True,
                'message': f'Keywords analysis failed: {keywords_result.error}',
                'total_keywords': 0,
                'position_distribution': {'top_3': 0, 'top_10': 0, 'top_50': 0, 'beyond_50': 0},
                'best_keywords': [],
                'opportunity_keywords': [],
                'data_source': 'Analysis failed'
            }
        else:

            keywords_data = _transform_keywords_data(keywords_result.data)
        

        print(f"DEBUG: Starting backlinks analysis for {url}")
        backlinks_result = backlinks_module.analyze(url)
        backlinks_data = backlinks_result.data
        print(f"DEBUG: Backlinks analysis result - Success: {backlinks_result.success}")
        print(f"DEBUG: Backlinks data keys: {list(backlinks_data.keys())}")
        if backlinks_data.get('total_backlinks'):
            print(f"DEBUG: Total backlinks found: {backlinks_data.get('total_backlinks')}")
        if backlinks_data.get('placeholder'):
            print(f"DEBUG: Using placeholder data: {backlinks_data.get('message')}")
        

        llm_rec_data = llm_recommendations.generate_recommendations(
            url, technical_data, content_data, keywords_data, backlinks_data
        )
        

        competitor_data = []
        for comp_url in competitor_list:
            comp_technical = technical_module.analyze(comp_url)
            comp_content = content_module.analyze(comp_url, quick_scan=True)
            

            comp_keywords_result = keywords_module.analyze(comp_url, competitor_domains=[], quick_scan=True)
            if comp_keywords_result.success:
                comp_keywords = _transform_keywords_data(comp_keywords_result.data)
            else:
                comp_keywords = {
                    'placeholder': True, 
                    'message': f'Keywords analysis failed: {comp_keywords_result.error}',
                    'total_keywords': 0,
                    'position_distribution': {'top_3': 0, 'top_10': 0, 'top_50': 0, 'beyond_50': 0},
                    'best_keywords': [],
                    'opportunity_keywords': [],
                    'data_source': 'Analysis failed'
                }
            

            comp_backlinks_result = backlinks_module.analyze(comp_url, quick_scan=True)
            comp_backlinks = comp_backlinks_result.data
            
            competitor_data.append({
                'url': comp_url,
                'technical': comp_technical,
                'content': comp_content,
                'keywords': comp_keywords,
                'backlinks': comp_backlinks
            })
        

        report_html = report_gen.generate_html_report(
            url=url,
            technical_data=technical_data,
            content_data=content_data,
            competitor_data=competitor_data,
            keywords_data=keywords_data,
            backlinks_data=backlinks_data,
            llm_recommendations=llm_rec_data,
            include_charts=True
        )
        

        reports_store[report_id] = {
            'url': url,
            'html': report_html,
            'technical_data': technical_data,
            'content_data': content_data,
            'keywords_data': keywords_data,
            'backlinks_data': backlinks_data,
            'llm_recommendations': llm_rec_data,
            'competitor_data': competitor_data
        }
        
        return jsonify({
            'success': True,
            'report_id': report_id,
            'redirect_url': f'/report/{report_id}'
        })
        
    except Exception as e:
        return jsonify({'error': f'Error generating report: {str(e)}'}), 500

@app.route('/report/<report_id>')
def view_report(report_id):
    if report_id not in reports_store:
        return redirect(url_for('index'))
    
    report_data = reports_store[report_id]
    return render_template('report.html', 
                         report_html=report_data['html'],
                         report_id=report_id,
                         url=report_data['url'])

@app.route('/download/<report_id>')
def download_html(report_id):
    if report_id not in reports_store:
        return jsonify({'error': 'Report not found'}), 404
    
    report_data = reports_store[report_id]
    
    with tempfile.NamedTemporaryFile(mode='w', suffix='.html', delete=False) as f:
        f.write(report_data['html'])
        temp_path = f.name
    
    filename = f"seo_report_{report_data['url'].replace('https://', '').replace('http://', '').replace('/', '_')}.html"
    
    return send_file(temp_path, as_attachment=True, download_name=filename, mimetype='text/html')

# PDF Download removed - now using browser print functionality

def _analyze_with_gsc(url: str, competitor_domains: List[str]):
    """Analyze keywords using GSC as primary source"""
    try:
        gsc_tokens = session.get('gsc_tokens', {})
        
        if not gsc_tokens.get('access_token'):
            return keywords_module.analyze(url, competitor_domains=competitor_domains)
        
        # Fetch GSC data using the updated method
        gsc_data = gsc_client.get_search_analytics(gsc_tokens)
        transformed_data = gsc_client.transform_gsc_data(gsc_data, urlparse(url).netloc)
        
        # Update session with potentially refreshed tokens
        session['gsc_tokens'] = gsc_tokens
        
        from modules.keywords import ModuleResult
        return ModuleResult(success=True, data=transformed_data)
        
    except Exception as e:
        print(f"GSC analysis failed: {e}")
        return keywords_module.analyze(url, competitor_domains=competitor_domains)

app._analyze_with_gsc = _analyze_with_gsc

@app.route('/auth/gsc/start')
def gsc_auth_start():
    """Start GSC OAuth flow"""
    if not gsc_client:
        return jsonify({'error': 'Google Search Console integration not available. Install: pip install google-api-python-client google-auth-oauthlib google-auth'}), 500
    
    try:
        auth_url = gsc_client.get_auth_url()
        return redirect(auth_url)
    except Exception as e:
        return jsonify({'error': f'OAuth setup failed: {str(e)}'}), 500

@app.route('/auth/gsc/callback')
def gsc_auth_callback():
    """Handle GSC OAuth callback"""
    auth_code = request.args.get('code')
    error = request.args.get('error')
    
    if error:
        return redirect(url_for('index', error=f'OAuth error: {error}'))
    
    if not auth_code:
        return redirect(url_for('index', error='No authorization code received'))
    
    try:
        tokens = gsc_client.exchange_code(auth_code)
        session['gsc_tokens'] = tokens
        return redirect(url_for('index', success='Google Search Console connected successfully'))
    except Exception as e:
        return redirect(url_for('index', error=f'Token exchange failed: {str(e)}'))

@app.route('/auth/gsc/status')
def gsc_auth_status():
    """Check GSC authentication status"""
    has_tokens = 'gsc_tokens' in session
    property_url = gsc_client.property_url
    
    return jsonify({
        'authenticated': has_tokens,
        'property_url': property_url,
        'client_configured': bool(gsc_client.client_id and gsc_client.client_secret)
    })

if __name__ == '__main__':
    app.run(debug=False, host='0.0.0.0', port=7860)