3v324v23 commited on
Commit
fd20001
·
1 Parent(s): b2dc9a7

Add: competitor intel analyzer (SerpAPI+PageSpeed+Groq), keyword tracking, smart alerts, scheduler, bulk crawl, email reports, settings API, professional PDF reports, fix .gitignore

Browse files
.gitignore CHANGED
@@ -6,5 +6,14 @@ __pycache__/
6
  output/ads_config.json
7
  output/users.db
8
  output/jobs.db
 
 
 
 
 
 
 
 
9
  .venv/
10
  *.egg-info/
 
 
6
  output/ads_config.json
7
  output/users.db
8
  output/jobs.db
9
+ output/job-*/
10
+ output/run-*/
11
+ output/snapshot-*.json
12
+ output/history.json
13
+ output/analysis.json
14
+ output/audit.json
15
+ output/recommendations.json
16
+ output/schema.jsonld
17
  .venv/
18
  *.egg-info/
19
+ Settings.webp
frontend/competitor-intel.html ADDED
@@ -0,0 +1,424 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!doctype html>
2
+ <html lang="ar" dir="rtl">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta name="viewport" content="width=device-width,initial-scale=1"/>
6
+ <title>محلل المنافسين الذكي — GEO Platform</title>
7
+ <link href="https://fonts.googleapis.com/css2?family=Cairo:wght@400;600;700;800&display=swap" rel="stylesheet">
8
+ <link rel="stylesheet" href="/theme.css"/>
9
+ <style>
10
+ body { font-family: 'Cairo', sans-serif; }
11
+
12
+ .config-card {
13
+ background: var(--surface);
14
+ border: 1px solid var(--border);
15
+ border-radius: 24px;
16
+ padding: 36px;
17
+ backdrop-filter: blur(20px);
18
+ position: relative;
19
+ overflow: hidden;
20
+ }
21
+ .config-card::before {
22
+ content: '';
23
+ position: absolute; top: -1px; left: 10%; right: 10%; height: 2px;
24
+ background: var(--accent-grad);
25
+ }
26
+
27
+ .form-grid {
28
+ display: grid;
29
+ grid-template-columns: 1fr 1fr;
30
+ gap: 16px;
31
+ margin-top: 24px;
32
+ }
33
+ .form-group { display: flex; flex-direction: column; gap: 6px; }
34
+ .form-group label { font-size: 13px; color: var(--muted); font-weight: 600; }
35
+ .form-group input, .form-group select {
36
+ background: rgba(0,0,0,0.4); border: 1px solid var(--border);
37
+ color: white; padding: 14px 18px; border-radius: 12px;
38
+ font-size: 14px; font-family: 'Cairo', sans-serif;
39
+ transition: all 0.3s;
40
+ }
41
+ .form-group input:focus, .form-group select:focus {
42
+ outline: none; border-color: var(--accent);
43
+ box-shadow: 0 0 20px rgba(0,242,255,0.1);
44
+ }
45
+ .form-group.full { grid-column: 1 / -1; }
46
+
47
+ .btn-analyze {
48
+ background: var(--accent-grad); color: #000;
49
+ font-family: 'Cairo', sans-serif; font-weight: 800; font-size: 15px;
50
+ padding: 16px 40px; border-radius: 16px; border: none; cursor: pointer;
51
+ box-shadow: 0 8px 25px rgba(0,242,255,0.3);
52
+ transition: all 0.3s; margin-top: 8px;
53
+ }
54
+ .btn-analyze:hover { transform: translateY(-3px); box-shadow: 0 15px 40px rgba(0,242,255,0.5); }
55
+ .btn-analyze:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
56
+
57
+ /* Results */
58
+ #results { display: none; margin-top: 32px; }
59
+
60
+ .your-card {
61
+ background: rgba(0,242,255,0.04);
62
+ border: 1px solid rgba(0,242,255,0.2);
63
+ border-radius: 20px; padding: 24px; margin-bottom: 24px;
64
+ }
65
+
66
+ .comp-grid {
67
+ display: grid;
68
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
69
+ gap: 20px; margin-top: 20px;
70
+ }
71
+ .comp-card {
72
+ background: var(--surface);
73
+ border: 1px solid var(--border);
74
+ border-radius: 20px; padding: 24px;
75
+ transition: all 0.4s cubic-bezier(0.16,1,0.3,1);
76
+ position: relative; overflow: hidden;
77
+ }
78
+ .comp-card:hover {
79
+ border-color: rgba(0,242,255,0.3);
80
+ transform: translateY(-6px);
81
+ box-shadow: 0 20px 60px rgba(0,0,0,0.4);
82
+ }
83
+ .comp-rank {
84
+ position: absolute; top: 16px; left: 16px;
85
+ width: 32px; height: 32px; border-radius: 50%;
86
+ background: var(--accent-grad); color: #000;
87
+ font-weight: 900; font-size: 13px;
88
+ display: flex; align-items: center; justify-content: center;
89
+ }
90
+ .comp-domain { font-weight: 800; font-size: 16px; margin-bottom: 6px; padding-right: 8px; }
91
+ .comp-snippet { font-size: 12px; color: var(--muted); line-height: 1.6; margin-bottom: 16px; }
92
+
93
+ .speed-grid {
94
+ display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px;
95
+ }
96
+ .speed-item { text-align: center; }
97
+ .speed-score {
98
+ font-size: 22px; font-weight: 900;
99
+ display: block; margin-bottom: 2px;
100
+ }
101
+ .speed-label { font-size: 10px; color: var(--muted); text-transform: uppercase; }
102
+
103
+ .vitals-row {
104
+ display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px;
105
+ }
106
+ .vital-badge {
107
+ font-size: 11px; padding: 3px 8px; border-radius: 6px;
108
+ background: rgba(255,255,255,0.04); border: 1px solid var(--border);
109
+ color: var(--muted);
110
+ }
111
+
112
+ /* AI Analysis */
113
+ .ai-card {
114
+ background: linear-gradient(135deg, rgba(124,58,237,0.08), rgba(0,242,255,0.04));
115
+ border: 1px solid rgba(124,58,237,0.2);
116
+ border-radius: 20px; padding: 28px; margin-top: 24px;
117
+ }
118
+ .ai-section { margin-bottom: 20px; }
119
+ .ai-section h4 { font-size: 14px; font-weight: 700; color: var(--accent); margin-bottom: 10px; }
120
+ .ai-item {
121
+ font-size: 13px; color: var(--muted); padding: 8px 12px;
122
+ background: rgba(255,255,255,0.02); border-radius: 8px;
123
+ margin-bottom: 6px; border-right: 3px solid var(--accent);
124
+ line-height: 1.6;
125
+ }
126
+
127
+ .data-sources {
128
+ display: flex; gap: 10px; flex-wrap: wrap; margin-top: 16px;
129
+ }
130
+ .source-badge {
131
+ font-size: 11px; padding: 4px 12px; border-radius: 20px;
132
+ font-weight: 700;
133
+ }
134
+ .source-active { background: rgba(0,255,149,0.1); color: var(--green); border: 1px solid rgba(0,255,149,0.2); }
135
+ .source-demo { background: rgba(255,204,0,0.1); color: var(--yellow); border: 1px solid rgba(255,204,0,0.2); }
136
+
137
+ .spinner-wrap { text-align: center; padding: 60px; }
138
+ .spinner {
139
+ width: 48px; height: 48px; border: 4px solid rgba(0,242,255,0.1);
140
+ border-top-color: var(--accent); border-radius: 50%;
141
+ animation: spin 0.8s linear infinite; margin: 0 auto 16px;
142
+ }
143
+ @keyframes spin { to { transform: rotate(360deg); } }
144
+
145
+ @media(max-width:768px) {
146
+ .form-grid { grid-template-columns: 1fr; }
147
+ .speed-grid { grid-template-columns: 1fr 1fr; }
148
+ }
149
+ </style>
150
+ </head>
151
+ <body>
152
+ <nav>
153
+ <div class="nav-logo">GEO<span>.</span>AI</div>
154
+ <div class="nav-links">
155
+ <a href="/jobs.html">المهام</a>
156
+ <a href="/recommendations.html">التوصيات</a>
157
+ <a href="/search.html">تحليل البحث</a>
158
+ <a href="/content_v2.html">المحتوى</a>
159
+ <a href="/ads.html">الإعلانات</a>
160
+ <a href="/competitor-intel.html" style="color:var(--accent)">المنافسون</a>
161
+ </div>
162
+ <a href="/" class="nav-cta">الرئيسية</a>
163
+ </nav>
164
+
165
+ <div class="wrap" style="padding-top:100px;max-width:1200px">
166
+
167
+ <div class="stagger-item" style="animation-delay:0.1s;margin-bottom:32px">
168
+ <h1 class="shine-text" style="font-size:42px;margin-bottom:10px">محلل المنافسين الذكي</h1>
169
+ <p style="color:var(--muted);font-size:16px">اكتشف منافسيك الحقيقيين في منطقتك المستهدفة مع مقارنة الأداء وتحليل الذكاء الاصطناعي</p>
170
+ <div class="data-sources" style="margin-top:12px">
171
+ <span class="source-badge source-active"> Google PageSpeed — مجاني 100%</span>
172
+ <span id="serp-badge" class="source-badge source-demo">⚡ SerpAPI — 100 بحث/شهر مجاناً</span>
173
+ <span id="ai-badge" class="source-badge source-demo"> Groq AI — تحليل استراتيجي</span>
174
+ </div>
175
+ </div>
176
+
177
+ <!-- Config Card -->
178
+ <div class="config-card stagger-item" style="animation-delay:0.2s">
179
+ <h2 style="margin:0 0 4px;font-size:22px">إعداد التحليل</h2>
180
+ <p style="color:var(--muted);font-size:14px;margin:0">أدخل موقعك وحدد المنطقة للعثور على منافسيك الحقيقيين</p>
181
+
182
+ <div class="form-grid">
183
+ <div class="form-group full">
184
+ <label>رابط موقعك</label>
185
+ <input id="yourUrl" type="url" placeholder="https://rabhanagency.com" value=""/>
186
+ </div>
187
+ <div class="form-group">
188
+ <label>المنطقة المستهدفة</label>
189
+ <select id="region">
190
+ <option value="Saudi Arabia"> السعودية</option>
191
+ <option value="Egypt"> مصر</option>
192
+ <option value="UAE"> الإمارات</option>
193
+ <option value="Kuwait"> الكويت</option>
194
+ <option value="Jordan"> الأردن</option>
195
+ <option value="Global"> عالمي</option>
196
+ </select>
197
+ </div>
198
+ <div class="form-group">
199
+ <label>المجال / النيش (اختياري)</label>
200
+ <input id="industry" placeholder="مثال: تسويق رقمي، تجارة إلكترونية"/>
201
+ </div>
202
+ <div class="form-group">
203
+ <label>عدد المنافسين</label>
204
+ <select id="count">
205
+ <option value="5">5 منافسين</option>
206
+ <option value="7" selected>7 منافسين</option>
207
+ <option value="10">10 منافسين</option>
208
+ </select>
209
+ </div>
210
+ <div class="form-group" style="align-self:end">
211
+ <button class="btn-analyze" id="analyzeBtn" onclick="runAnalysis()">
212
+ 🔍 تحليل المنافسين
213
+ </button>
214
+ </div>
215
+ </div>
216
+ <div id="status" style="margin-top:12px;font-size:13px;color:var(--muted)"></div>
217
+ </div>
218
+
219
+ <!-- Results -->
220
+ <div id="results">
221
+
222
+ <!-- Your site -->
223
+ <div class="your-card stagger-item">
224
+ <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
225
+ <div>
226
+ <div style="font-size:12px;color:var(--accent);font-weight:700;margin-bottom:4px">موقعك</div>
227
+ <div id="yourDomain" style="font-size:20px;font-weight:800"></div>
228
+ </div>
229
+ <div id="yourPosition" style="font-size:13px;color:var(--muted)"></div>
230
+ </div>
231
+ <div class="speed-grid" id="yourSpeed"></div>
232
+ <div class="vitals-row" id="yourVitals"></div>
233
+ </div>
234
+
235
+ <!-- Competitors -->
236
+ <h2 style="font-size:20px;font-weight:800;margin-bottom:4px">
237
+ المنافسون المكتشفون
238
+ <span id="compCount" style="color:var(--accent);font-size:16px"></span>
239
+ </h2>
240
+ <p style="color:var(--muted);font-size:13px;margin-bottom:0" id="dataSourceNote"></p>
241
+ <div class="comp-grid" id="compGrid"></div>
242
+
243
+ <!-- AI Analysis -->
244
+ <div class="ai-card stagger-item" id="aiCard" style="display:none">
245
+ <h3 style="margin:0 0 20px;font-size:20px;font-weight:800;color:#a78bfa">
246
+ التحليل الاستراتيجي بالذكاء الاصطناعي
247
+ </h3>
248
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:20px" id="aiGrid"></div>
249
+ <div id="aiSummary" style="margin-top:16px;padding:14px;background:rgba(255,255,255,0.02);border-radius:10px;font-size:13px;color:var(--muted);line-height:1.7"></div>
250
+ </div>
251
+
252
+ </div>
253
+
254
+ </div>
255
+
256
+ <script>
257
+ // Check stored API keys
258
+ const storedKeys = JSON.parse(localStorage.getItem('geo_keys') || '{}');
259
+ if (storedKeys.serpapi || storedKeys.serp) {
260
+ document.getElementById('serp-badge').className = 'source-badge source-active';
261
+ document.getElementById('serp-badge').textContent = ' SerpAPI — متصل';
262
+ }
263
+ if (storedKeys.groq || storedKeys.openai) {
264
+ document.getElementById('ai-badge').className = 'source-badge source-active';
265
+ document.getElementById('ai-badge').textContent = ' AI — متصل';
266
+ }
267
+
268
+ // Pre-fill from last job
269
+ const lastJob = JSON.parse(localStorage.getItem('active_geo_job') || 'null');
270
+ if (lastJob?.url) document.getElementById('yourUrl').value = lastJob.url;
271
+
272
+ function scoreColor(s) {
273
+ if (s === null || s === undefined) return 'var(--muted)';
274
+ return s >= 80 ? 'var(--green)' : s >= 50 ? 'var(--yellow)' : 'var(--red)';
275
+ }
276
+
277
+ function renderSpeedGrid(ps, containerId, vitalsId) {
278
+ const el = document.getElementById(containerId);
279
+ const vitalsEl = document.getElementById(vitalsId);
280
+ if (!ps) { el.innerHTML = '<div style="color:var(--muted);font-size:13px">جارٍ تحميل بيانات الأداء...</div>'; return; }
281
+
282
+ const metrics = [
283
+ { label: 'الأداء', val: ps.performance },
284
+ { label: 'SEO', val: ps.seo },
285
+ { label: 'إمكانية الوصول', val: ps.accessibility },
286
+ ];
287
+ el.innerHTML = metrics.map(m => `
288
+ <div class="speed-item">
289
+ <span class="speed-score" style="color:${scoreColor(m.val)}">${m.val ?? '—'}</span>
290
+ <span class="speed-label">${m.label}</span>
291
+ </div>`).join('');
292
+
293
+ vitalsEl.innerHTML = [
294
+ ['FCP', ps.fcp], ['LCP', ps.lcp], ['CLS', ps.cls], ['TBT', ps.tbt]
295
+ ].map(([k,v]) => `<span class="vital-badge">${k}: ${v}</span>`).join('');
296
+ }
297
+
298
+ function renderCompetitors(competitors) {
299
+ const grid = document.getElementById('compGrid');
300
+ grid.innerHTML = '';
301
+ competitors.forEach((c, i) => {
302
+ const ps = c.pagespeed || {};
303
+ const card = document.createElement('div');
304
+ card.className = 'comp-card stagger-item';
305
+ card.style.animationDelay = `${i * 0.05}s`;
306
+ card.innerHTML = `
307
+ <div class="comp-rank">${c.position || i+1}</div>
308
+ <div class="comp-domain">${c.domain}</div>
309
+ <div class="comp-snippet">${c.snippet || c.title || ''}</div>
310
+ <div class="speed-grid">
311
+ ${[['الأداء', ps.performance], ['SEO', ps.seo], ['وصول', ps.accessibility]].map(([l,v]) => `
312
+ <div class="speed-item">
313
+ <span class="speed-score" style="color:${scoreColor(v)}">${v ?? '—'}</span>
314
+ <span class="speed-label">${l}</span>
315
+ </div>`).join('')}
316
+ </div>
317
+ <div class="vitals-row">
318
+ ${[['FCP', ps.fcp], ['LCP', ps.lcp], ['CLS', ps.cls]].map(([k,v]) =>
319
+ `<span class="vital-badge">${k}: ${v}</span>`).join('')}
320
+ </div>
321
+ <div style="margin-top:12px">
322
+ <a href="https://${c.domain}" target="_blank"
323
+ style="font-size:12px;color:var(--accent);text-decoration:none">
324
+ زيارة الموقع ↗
325
+ </a>
326
+ </div>`;
327
+ grid.appendChild(card);
328
+ });
329
+ }
330
+
331
+ function renderAIAnalysis(ai) {
332
+ if (!ai) return;
333
+ document.getElementById('aiCard').style.display = 'block';
334
+ const grid = document.getElementById('aiGrid');
335
+
336
+ const sections = [
337
+ { title: ' موقعك في السوق', key: 'market_position', single: true },
338
+ { title: ' فرصك الذهبية', key: 'your_opportunities' },
339
+ { title: '⚡ انتصارات سريعة', key: 'quick_wins' },
340
+ { title: ' كلمات مفتاحية مقترحة', key: 'recommended_keywords' },
341
+ { title: ' التهديدات', key: 'threats' },
342
+ { title: ' مميزات المنافسين', key: 'key_differentiators' },
343
+ ];
344
+
345
+ grid.innerHTML = sections.map(s => {
346
+ const val = ai[s.key];
347
+ const items = s.single
348
+ ? `<div class="ai-item" style="font-size:16px;font-weight:800;color:var(--accent)">${val}</div>`
349
+ : (Array.isArray(val) ? val.map(v => `<div class="ai-item">${v}</div>`).join('') : '');
350
+ return `<div class="ai-section"><h4>${s.title}</h4>${items}</div>`;
351
+ }).join('');
352
+
353
+ if (ai.market_summary) {
354
+ document.getElementById('aiSummary').innerHTML =
355
+ `<strong style="color:var(--text)">ملخص السوق:</strong> ${ai.market_summary}`;
356
+ }
357
+ }
358
+
359
+ async function runAnalysis() {
360
+ const url = document.getElementById('yourUrl').value.trim();
361
+ if (!url) { document.getElementById('status').textContent = 'أدخل رابط موقعك أولاً'; return; }
362
+
363
+ const btn = document.getElementById('analyzeBtn');
364
+ const status = document.getElementById('status');
365
+ btn.disabled = true;
366
+ btn.textContent = '⏳ جارٍ التحليل...';
367
+ document.getElementById('results').style.display = 'none';
368
+ status.innerHTML = '<span class="shine-text">⏳ جارٍ البحث عن المنافسين وقياس الأداء...</span>';
369
+
370
+ try {
371
+ const keys = JSON.parse(localStorage.getItem('geo_keys') || '{}');
372
+ const resp = await fetch('/api/competitor/intelligence', {
373
+ method: 'POST',
374
+ headers: { 'Content-Type': 'application/json' },
375
+ body: JSON.stringify({
376
+ url,
377
+ region: document.getElementById('region').value,
378
+ industry: document.getElementById('industry').value,
379
+ count: parseInt(document.getElementById('count').value),
380
+ api_keys: keys
381
+ })
382
+ });
383
+ const data = await resp.json();
384
+ if (!data.ok) { status.textContent = ' خطأ: ' + data.error; btn.disabled = false; btn.textContent = '🔍 تحليل المنافسين'; return; }
385
+
386
+ const r = data.result;
387
+ status.innerHTML = `<span style="color:var(--green)"> تم اكتشاف ${r.competitor_count} منافس</span>`;
388
+
389
+ // Data source badges
390
+ const note = [];
391
+ if (r.data_sources.serp) note.push(' SerpAPI: نتائج حقيقية من Google');
392
+ else note.push(' بدون SerpAPI: المنافسون مقترحون بالذكاء الاصطناعي');
393
+ if (r.data_sources.pagespeed) note.push(' PageSpeed: بيانات أداء حقيقية');
394
+ if (r.data_sources.ai) note.push(' AI: تحليل استراتيجي حقيقي');
395
+ else note.push(' أضف Groq API للتحليل الاستراتيجي');
396
+ document.getElementById('dataSourceNote').textContent = note.join(' · ');
397
+
398
+ // Your site
399
+ document.getElementById('yourDomain').textContent = r.your_domain;
400
+ renderSpeedGrid(r.your_pagespeed, 'yourSpeed', 'yourVitals');
401
+
402
+ // Competitors
403
+ document.getElementById('compCount').textContent = ` (${r.competitor_count})`;
404
+ renderCompetitors(r.competitors);
405
+
406
+ // AI Analysis
407
+ renderAIAnalysis(r.ai_analysis);
408
+
409
+ document.getElementById('results').style.display = 'block';
410
+ } catch (e) {
411
+ status.textContent = ' فشل الطلب: ' + e.message;
412
+ } finally {
413
+ btn.disabled = false;
414
+ btn.textContent = ' تحليل المنافسين';
415
+ }
416
+ }
417
+
418
+ // Allow Enter key
419
+ document.getElementById('yourUrl').addEventListener('keydown', e => {
420
+ if (e.key === 'Enter') runAnalysis();
421
+ });
422
+ </script>
423
+ </body>
424
+ </html>
frontend/index.html CHANGED
@@ -299,6 +299,12 @@
299
  <div class="card-arrow">عرض المهام ←</div>
300
  </a>
301
 
 
 
 
 
 
 
302
  </div>
303
  </section>
304
 
 
299
  <div class="card-arrow">عرض المهام ←</div>
300
  </a>
301
 
302
+ <a href="/competitor-intel.html" class="service-card stagger-item" style="animation-delay:0.45s">
303
+ <div class="card-title">محلل المنافسين الذكي</div>
304
+ <div class="card-desc">اكتشف منافسيك الحقيقيين في منطقتك مع مقارنة أداء السرعة والسيو وتحليل استراتيجي بالذكاء الاصطناعي لكل منافس.</div>
305
+ <div class="card-arrow">تحليل المنافسين ←</div>
306
+ </a>
307
+
308
  </div>
309
  </section>
310
 
frontend/recommendations.html CHANGED
@@ -164,6 +164,7 @@
164
  <a href="/content_v2.html">المحتوى</a>
165
  <a href="/ads.html">الإعلانات</a>
166
  <a href="/geo-toolkit.html">خدمات GEO</a>
 
167
  </div>
168
  <a href="/" class="nav-cta" id="navCta">الرئيسية</a>
169
  </nav>
 
164
  <a href="/content_v2.html">المحتوى</a>
165
  <a href="/ads.html">الإعلانات</a>
166
  <a href="/geo-toolkit.html">خدمات GEO</a>
167
+ <a href="/competitor-intel.html">المنافسون</a>
168
  </div>
169
  <a href="/" class="nav-cta" id="navCta">الرئيسية</a>
170
  </nav>
frontend/regional.html CHANGED
@@ -239,6 +239,7 @@
239
  <a href="/content_v2.html">المحتوى</a>
240
  <a href="/ads.html">الإعلانات</a>
241
  <a href="/geo-toolkit.html">خدمات GEO</a>
 
242
  </div>
243
  <a href="/index.html" class="nav-cta">الرئيسية</a>
244
  </nav>
 
239
  <a href="/content_v2.html">المحتوى</a>
240
  <a href="/ads.html">الإعلانات</a>
241
  <a href="/geo-toolkit.html">خدمات GEO</a>
242
+ <a href="/competitor-intel.html">المنافسون</a>
243
  </div>
244
  <a href="/index.html" class="nav-cta">الرئيسية</a>
245
  </nav>
frontend/search.html CHANGED
@@ -89,6 +89,7 @@
89
  <a href="/content_v2.html">المحتوى</a>
90
  <a href="/ads.html">الإعلانات</a>
91
  <a href="/geo-toolkit.html">خدمات GEO</a>
 
92
  </div>
93
  <a href="/" class="nav-cta" id="navCta">الرئيسية</a>
94
  </nav>
 
89
  <a href="/content_v2.html">المحتوى</a>
90
  <a href="/ads.html">الإعلانات</a>
91
  <a href="/geo-toolkit.html">خدمات GEO</a>
92
+ <a href="/competitor-intel.html">المنافسون</a>
93
  </div>
94
  <a href="/" class="nav-cta" id="navCta">الرئيسية</a>
95
  </nav>
output/analysis.json CHANGED
@@ -10,17 +10,17 @@
10
  }
11
  },
12
  "geo_score": {
13
- "score": 65,
14
- "status": "Needs Work",
15
  "breakdown": {
16
- "headings": 20,
17
- "density": 5,
18
- "entities": 20,
19
- "faq": 20,
20
  "ai_visibility": 0
21
  },
22
  "counts": {
23
- "critical": 3,
24
  "warnings": 0,
25
  "passed": 0
26
  }
 
10
  }
11
  },
12
  "geo_score": {
13
+ "score": 0,
14
+ "status": "Critical",
15
  "breakdown": {
16
+ "headings": 0,
17
+ "density": 0,
18
+ "entities": 0,
19
+ "faq": 0,
20
  "ai_visibility": 0
21
  },
22
  "counts": {
23
+ "critical": 2,
24
  "warnings": 0,
25
  "passed": 0
26
  }
output/audit.json CHANGED
@@ -1,832 +1,55 @@
1
  {
2
  "pages": [
3
  {
4
- "url": "https://abayanoir.com/",
5
- "title": "Abaya Noir Your Basics & More – abayanoir1",
6
- "headings": [
7
- {
8
- "tag": "h1",
9
- "text": ""
10
- },
11
- {
12
- "tag": "h2",
13
- "text": "New Arrivals"
14
- },
15
- {
16
- "tag": "h2",
17
- "text": "Shop by Category"
18
- },
19
- {
20
- "tag": "h3",
21
- "text": "Abayas"
22
- },
23
- {
24
- "tag": "h3",
25
- "text": "Casual"
26
- },
27
- {
28
- "tag": "h3",
29
- "text": "Winter"
30
- },
31
- {
32
- "tag": "h3",
33
- "text": "Basics"
34
- },
35
- {
36
- "tag": "h3",
37
- "text": "Swimwear"
38
- },
39
- {
40
- "tag": "h2",
41
- "text": "Best Sellers"
42
- },
43
- {
44
- "tag": "h2",
45
- "text": "Basics"
46
- },
47
- {
48
- "tag": "h3",
49
- "text": "When Elegance Meets Charm"
50
- },
51
- {
52
- "tag": "h3",
53
- "text": "Ramadan 2026"
54
- },
55
- {
56
- "tag": "h3",
57
- "text": "THE ESSENCE OF BLACK"
58
- },
59
- {
60
- "tag": "h3",
61
- "text": "ABAYA NOIR"
62
- },
63
- {
64
- "tag": "h2",
65
- "text": "Our Stores"
66
- },
67
- {
68
- "tag": "h3",
69
- "text": "City Stars"
70
- },
71
- {
72
- "tag": "h3",
73
- "text": "Mall El Arab"
74
- },
75
- {
76
- "tag": "h3",
77
- "text": "Rehab Mall 2"
78
- },
79
- {
80
- "tag": "h3",
81
- "text": "Mohandessen"
82
- },
83
- {
84
- "tag": "h3",
85
- "text": "Heliopolis"
86
- },
87
- {
88
- "tag": "h3",
89
- "text": "Alexandria"
90
- },
91
- {
92
- "tag": "h2",
93
- "text": "City Stars"
94
- },
95
- {
96
- "tag": "h2",
97
- "text": "Mall El Arab"
98
- },
99
- {
100
- "tag": "h2",
101
- "text": "Rehab Mall 2"
102
- },
103
- {
104
- "tag": "h2",
105
- "text": "Mohandessen"
106
- },
107
- {
108
- "tag": "h2",
109
- "text": "Heliopolis"
110
- },
111
- {
112
- "tag": "h2",
113
- "text": "Alexandria"
114
- },
115
- {
116
- "tag": "h4",
117
- "text": "YOUR CART (0)"
118
- },
119
- {
120
- "tag": "h5",
121
- "text": "Add Order Note"
122
- }
123
- ],
124
- "paragraphs": [
125
- "LOGIN",
126
- "New User?Register Now",
127
- "Wishlist",
128
- "Browse our latest releases, best sellers, and clearance items!",
129
- "FOR EVERYDAY WEAR",
130
- "Abaya Noir Store Locations",
131
- "Address38FR+X6V, Masaken Al Mohandesin, Nasr City, Cairo Governorate 4451701",
132
- "PHONE: 01289982763",
133
- "AddressPhase 2 , 3rd Floor store 3030",
134
- "PHONE: +2 (012) 7484 7469",
135
- "Address6 october , mall of arabia",
136
- "PHONE: +2 (012) 7484 7468",
137
- "AddressNew Cairo , rehab city , 2nd floor",
138
- "PHONE: +2 (012) 8998 2651",
139
- "Address39 Syria St. Mohandeseen",
140
- "PHONE: +2 (012) 8998 2789",
141
- "Address10 Nozha St., Saudia buildings",
142
- "PHONE: +2 (012) 8998 2763",
143
- "AddressSan stefano mall , 3rd floor",
144
- "PHONE: +2 (012) 8998 2794",
145
- "Address38FR+X6V, Masaken Al Mohandesin, Nasr City, Cairo Governorate 4451701",
146
- "PHONE: 01289982763",
147
- "AddressPhase 2 , 3rd Floor store 3030",
148
- "PHONE: +2 (012) 7484 7469",
149
- "Address6 october , mall of arabia",
150
- "PHONE: +2 (012) 7484 7468",
151
- "AddressNew Cairo , rehab city , 2nd floor",
152
- "PHONE: +2 (012) 8998 2651",
153
- "Address39 Syria St. Mohandeseen",
154
- "PHONE: +2 (012) 8998 2789",
155
- "Address10 Nozha St., Saudia buildings",
156
- "PHONE: +2 (012) 8998 2763",
157
- "AddressSan stefano mall , 3rd floor",
158
- "PHONE: +2 (012) 8998 2794",
159
- "1-2 DAYS",
160
- "Delivery to Doorstep",
161
- "100% SAFE",
162
- "Encrypted Connection",
163
- "ONLINE PAYMENT",
164
- "Secure Payment Solutions",
165
- "FREE RETURNS",
166
- "Track or cancel orders",
167
- "Our Stores",
168
- "Stay Connected",
169
- "Newsletter",
170
- "Enter your email to receive daily news and get 20% off coupon for all items. NO spam, we promise",
171
- "You don't have any items in your cart.",
172
- "Loading...",
173
- "What are you looking for?",
174
- "Ramadan 2026 Now in stores !",
175
- "dual fabric abaya shirtcafe"
176
- ],
177
- "links": [
178
- "https://abayanoir.com/account",
179
- "https://abayanoir.com/collections/best-sellers",
180
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe",
181
- "https://abayanoir.com/products/dual-fabric-abaya-shirt-cafe?section_id=quick-view",
182
- "https://abayanoir.com/products/silk-sleeveless-mint-abaya-with-hidden-zipper",
183
- "https://abayanoir.com/collections/accessories",
184
- "https://abayanoir.com/search",
185
- "https://abayanoir.com/products/wool-jacket?section_id=quick-view",
186
- "https://abayanoir.com/policies/shipping-policy",
187
- "https://abayanoir.com/products/flowy-abaya-dress",
188
- "https://abayanoir.com/collections/headwear",
189
- "https://abayanoir.com/products/back-pleated-long-shirt-black?section_id=quick-view",
190
- "https://abayanoir.com/products/flowy-abaya-dress?section_id=quick-view",
191
- "https://abayanoir.com/products/chiffon-scarf-with-satin-detail",
192
- "https://abayanoir.com/collections/new",
193
- "https://abayanoir.com/products/chiffon-ruffle-abaya-set-grey?section_id=quick-view",
194
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe?section_id=quick-view",
195
- "https://abayanoir.com/products/black-viscose-pants?section_id=quick-view",
196
- "https://abayanoir.com/collections/noir-originals",
197
- "https://abayanoir.com/products/leather-skirt-%D9%90a-line-2",
198
- "https://abayanoir.com/products/the-noir-prayer-dress",
199
- "https://abayanoir.com/products/back-pleated-long-shirt-black",
200
- "https://abayanoir.com/cart",
201
- "https://abayanoir.com/products/silk-teal-green-sleeveless-abaya?section_id=quick-view",
202
- "https://abayanoir.com/products/long-tricot-cardigan-3",
203
- "https://abayanoir.com/products/chiffon-ruffle-abaya-set-black?section_id=quick-view",
204
- "https://abayanoir.com/products/chiffon-ruffle-abaya-set-black",
205
- "https://abayanoir.com/products/cut-dress-with-diamod-shape-details-at-front",
206
- "https://abayanoir.com/products/pleated-faux-leather-skirt?section_id=quick-view",
207
- "https://abayanoir.com/collections/basics",
208
- "https://abayanoir.com/products/black-viscose-pants",
209
- "https://abayanoir.com/products/silk-sleeveless-mint-abaya-with-hidden-zipper?section_id=quick-view",
210
- "https://abayanoir.com/products/dark-blossom-prayer-dress?section_id=quick-view",
211
- "https://abayanoir.com/collections/up-to-60-off",
212
- "https://abayanoir.com/products/cotton-white-top?section_id=quick-view",
213
- "https://abayanoir.com/policies/privacy-policy",
214
- "https://abayanoir.com/",
215
- "https://abayanoir.com/collections/all",
216
- "https://abayanoir.com/products/back-pleated-long-shirt-white?section_id=quick-view",
217
- "https://abayanoir.com/collections/swimwear",
218
- "https://abayanoir.com/products/long-tricot-cardigan-1",
219
- "https://abayanoir.com/products/flowy-abaya-with-lace-detail?section_id=quick-view",
220
- "https://abayanoir.com/products/dark-blossom-prayer-dress",
221
- "https://abayanoir.com/products/long-tricot-cardigan-3?section_id=quick-view",
222
- "https://abayanoir.com/customer_authentication/redirect?locale=en®ion_country=EG",
223
- "https://abayanoir.com/products/basic-cotton-top-in-white",
224
- "https://abayanoir.com/products/soft-touch-cargo-set-beige",
225
- "https://abayanoir.com/collections/abaya",
226
- "https://abayanoir.com/products/the-noir-prayer-dress?section_id=quick-view",
227
- "https://abayanoir.com/products/leather-skirt-%D9%90a-line-2?section_id=quick-view",
228
- "https://abayanoir.com/products/cotton-white-top",
229
- "https://abayanoir.com/products/back-pleated-long-shirt-white",
230
- "https://abayanoir.com/products/soft-touch-cargo-set-black",
231
- "https://abayanoir.com/products/flowing-textured-wide-dress-cafe",
232
- "https://abayanoir.com/products/flowing-textured-wide-dress-cafe?section_id=quick-view",
233
- "https://abayanoir.com/products/pleated-faux-leather-skirt",
234
- "https://abayanoir.com/products/wool-jacket",
235
- "https://abayanoir.com/products/cut-dress-with-diamod-shape-details-at-front?section_id=quick-view",
236
- "https://abayanoir.com/collections/shirts-blouses",
237
- "https://abayanoir.com/products/basic-cotton-top-in-white?section_id=quick-view",
238
- "https://abayanoir.com/#cartNote",
239
- "https://abayanoir.com/products/soft-touch-cargo-set-black?section_id=quick-view",
240
- "https://abayanoir.com/products/cotton-top?section_id=quick-view",
241
- "https://abayanoir.com/products/soft-touch-cargo-set-beige?section_id=quick-view",
242
- "https://abayanoir.com/products/cotton-top",
243
- "https://abayanoir.com/#cartCoupon",
244
- "https://abayanoir.com/products/long-tricot-cardigan-1?section_id=quick-view",
245
- "https://abayanoir.com/products/chiffon-scarf-with-satin-detail?section_id=quick-view",
246
- "https://abayanoir.com/collections/bottoms",
247
- "https://abayanoir.com/products/flowy-abaya-with-lace-detail",
248
- "https://abayanoir.com/products/silk-teal-green-sleeveless-abaya",
249
- "https://abayanoir.com/policies/refund-policy",
250
- "https://abayanoir.com/collections/niqab",
251
- "https://abayanoir.com/products/dual-fabric-abaya-shirt-cafe",
252
- "https://abayanoir.com/products/chiffon-ruffle-abaya-set-grey"
253
- ]
254
- },
255
- {
256
- "url": "https://abayanoir.com/collections/best-sellers",
257
- "title": "Best Sellers – abayanoir1",
258
- "headings": [
259
- {
260
- "tag": "h1",
261
- "text": "Best Sellers"
262
- },
263
- {
264
- "tag": "h4",
265
- "text": "YOUR CART (0)"
266
- },
267
- {
268
- "tag": "h5",
269
- "text": "Add Order Note"
270
- }
271
- ],
272
  "paragraphs": [
273
- "LOGIN",
274
- "New User?Register Now",
275
- "Wishlist",
276
- "Our Stores",
277
- "Stay Connected",
278
- "Newsletter",
279
- "Enter your email to receive daily news and get 20% off coupon for all items. NO spam, we promise",
280
- "You don't have any items in your cart.",
281
- "Loading...",
282
- "What are you looking for?",
283
- "Ramadan 2026 Now in stores !",
284
- "lace trim open abaya"
285
  ],
286
  "links": [
287
- "https://abayanoir.com/account",
288
- "https://abayanoir.com/collections/best-sellers",
289
- "https://abayanoir.com/collections/best-sellers/products/leather-skirt-%D9%90a-line-2",
290
- "https://abayanoir.com/products/dual-fabric-abaya-shirt-cafe?section_id=quick-view",
291
- "https://abayanoir.com/collections/accessories",
292
- "https://abayanoir.com/search",
293
- "https://abayanoir.com/products/wool-jacket?section_id=quick-view",
294
- "https://abayanoir.com/policies/shipping-policy",
295
- "https://abayanoir.com/collections/headwear",
296
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe?section_id=quick-view",
297
- "https://abayanoir.com/collections/new",
298
- "https://abayanoir.com/collections/noir-originals",
299
- "https://abayanoir.com/collections/best-sellers/products/cut-dress-with-diamod-shape-details-at-front",
300
- "https://abayanoir.com/collections/best-sellers/products/long-tricot-cardigan-3",
301
- "https://abayanoir.com/cart",
302
- "https://abayanoir.com/collections/best-sellers/products/flowing-textured-wide-dress-cafe",
303
- "https://abayanoir.com/collections/dress",
304
- "https://abayanoir.com/collections/sets",
305
- "https://abayanoir.com/products/pleated-faux-leather-skirt?section_id=quick-view",
306
- "https://abayanoir.com/collections/basics",
307
- "https://abayanoir.com/collections/up-to-60-off",
308
- "https://abayanoir.com/collections/best-sellers/products/dual-fabric-abaya-shirt-cafe",
309
- "https://abayanoir.com/collections/best-sellers/products/long-tricot-cardigan-1",
310
- "https://abayanoir.com/policies/privacy-policy",
311
- "https://abayanoir.com/",
312
- "https://abayanoir.com/collections/best-sellers/products/chiffon-scarf-with-mat-satin-detail",
313
- "https://abayanoir.com/products/niqab-malaky-short?section_id=quick-view",
314
- "https://abayanoir.com/collections/all",
315
- "https://abayanoir.com/collections/swimwear",
316
- "https://abayanoir.com/collections/best-sellers#cartCoupon",
317
- "https://abayanoir.com/collections/best-sellers/products/comfy-jersey-pants",
318
- "https://abayanoir.com/collections/gown-isdal",
319
- "https://abayanoir.com/collections/best-sellers/products/squared-scarf",
320
- "https://abayanoir.com/products/long-tricot-cardigan-3?section_id=quick-view",
321
- "https://abayanoir.com/collections/best-sellers/products/pleated-faux-leather-skirt",
322
- "https://abayanoir.com/customer_authentication/redirect?locale=en®ion_country=EG",
323
- "https://abayanoir.com/collections/best-sellers/products/fronce-abaya-made-with-crepe",
324
- "https://abayanoir.com/collections/best-sellers/products/crepe-abaya-with-satin-lining-detail-on-the-front",
325
- "https://abayanoir.com/collections/best-sellers/products/long-cozy-milton-coat-with-hood",
326
- "https://abayanoir.com/collections/abaya",
327
- "https://abayanoir.com/products/leather-skirt-%D9%90a-line-2?section_id=quick-view",
328
- "https://abayanoir.com/collections/best-sellers/products/sport-abaya-with-touch-of-indigo-or-mint-green-cotton",
329
- "https://abayanoir.com/collections/best-sellers?page=2",
330
- "https://abayanoir.com/products/crepe-abaya-with-satin-lining-detail-on-the-front?section_id=quick-view",
331
- "https://abayanoir.com/products/flowing-textured-wide-dress-cafe?section_id=quick-view",
332
- "https://abayanoir.com/products/cut-dress-with-diamod-shape-details-at-front?section_id=quick-view",
333
- "https://abayanoir.com/products/lace-trim-open-abaya",
334
- "https://abayanoir.com/collections/best-sellers?sort_by=manual&type=grid",
335
- "https://abayanoir.com/collections/best-sellers/products/niqab-malaky-short",
336
- "https://abayanoir.com/collections/shirts-blouses",
337
- "https://abayanoir.com/collections/best-sellers/products/chiffon-scarf-with-satin-detail",
338
- "https://abayanoir.com/collections/best-sellers/products/wool-jacket",
339
- "https://abayanoir.com/collections/best-sellers#cartNote",
340
- "https://abayanoir.com/products/long-cozy-milton-coat-with-hood?section_id=quick-view",
341
- "https://abayanoir.com/collections/best-sellers/products/sleeveless-crepe-dress",
342
- "https://abayanoir.com/products/sport-abaya-with-touch-of-indigo-or-mint-green-cotton?section_id=quick-view",
343
- "https://abayanoir.com/products/long-tricot-cardigan-1?section_id=quick-view",
344
- "https://abayanoir.com/products/chiffon-scarf-with-satin-detail?section_id=quick-view",
345
- "https://abayanoir.com/products/comfy-jersey-pants?section_id=quick-view",
346
- "https://abayanoir.com/collections/bottoms",
347
- "https://abayanoir.com/policies/refund-policy",
348
- "https://abayanoir.com/products/sleeveless-crepe-dress?section_id=quick-view",
349
- "https://abayanoir.com/collections/niqab",
350
- "https://abayanoir.com/collections/cardigans"
351
  ]
352
  },
353
  {
354
- "url": "https://abayanoir.com/products/fronce-abaya-made-with-crepe",
355
- "title": "Fronce Abaya made with crepe abayanoir1",
356
- "headings": [
357
- {
358
- "tag": "h1",
359
- "text": "Fronce Abaya made with crepe"
360
- },
361
- {
362
- "tag": "h3",
363
- "text": "Notify me when available"
364
- },
365
- {
366
- "tag": "h3",
367
- "text": "Fronce Abaya made with crepe"
368
- },
369
- {
370
- "tag": "h3",
371
- "text": "PRODUCT DETAILS"
372
- },
373
- {
374
- "tag": "h3",
375
- "text": "SHIPPING & RETURNS"
376
- },
377
- {
378
- "tag": "h2",
379
- "text": "Shipment Timeframe"
380
- },
381
- {
382
- "tag": "h2",
383
- "text": "Return & REFUND"
384
- },
385
- {
386
- "tag": "h2",
387
- "text": "RECOMMENDED PRODUCTS"
388
- },
389
- {
390
- "tag": "h2",
391
- "text": "RECENTLY VIEWED PRODUCTS"
392
- },
393
- {
394
- "tag": "h4",
395
- "text": "YOUR CART (0)"
396
- },
397
- {
398
- "tag": "h5",
399
- "text": "Add Order Note"
400
- }
401
- ],
402
  "paragraphs": [
403
- "LOGIN",
404
- "New User?Register Now",
405
- "Wishlist",
406
- "You have already submited form for XX-Small",
407
- "Fronce Abaya made with crepe",
408
- "Delivery time frame : 5 to 7 working days",
409
- "عباية فرونس مصنوعة من الكريب",
410
- "If you place your order before 11:59 pm EST (SUNDAY-THURSDAY), please allow a processing time of 4-5 days for your order to ship, dependent on busy holidays, inclement weather, carrier delays, and inventory count days. Some orders will be shipped after 4-7 working days as some items need to be arranged from different warehouses. Additional shipping rates, options, and estimated delivery will be displayed at the time of checkout depending on the location and service you choose.We are always striving to find better logistic methods to minimize shipping costs for our customers, and to reduce the CO2 footprint on the environment. In our effort to ensure efficient delivery and eco-friendly logistics, this can lead to an additional 5-6 days delay in shipping, depending on the option you have selected. Furthermore, based on the specific items you’ve ordered and the particular fulfillment center they are located in, this could also influence the delivery time.",
411
- "Please send your returned item to the following address: 10 Saudia Buildings ,Kelani Mohamed Kelani St, from Nozha St, Nasr city , cairo , Egyptand ensure that the package is clearly marked as “RETURNED GOODS” and declared at the same value as the invoice enclosed in the original package.",
412
- "Please handle all goods with care and take necessary precautions to ensure they are returned to us in their original condition, unworn, undamaged, and with tags intact, within 14 days of receiving the item. We reserve the right not to process a return if the item is not received within 14 days. Please do not use unnecessary force when trying on clothing and refrain from wearing perfume or deodorant, as these may leave marks or scents that render the item ineligible for return. We cannot issue refunds on items with broken hygiene seals or removed tickets.",
413
- "Our Stores",
414
- "Stay Connected",
415
- "Newsletter",
416
- "Enter your email to receive daily news and get 20% off coupon for all items. NO spam, we promise",
417
- "You don't have any items in your cart.",
418
- "Loading...",
419
- "What are you looking for?",
420
- "Ramadan 2026 Now in stores !",
421
- "Front-Zip Elevated Cape black"
422
  ],
423
  "links": [
424
- "https://abayanoir.com/account",
425
- "https://abayanoir.com/cdn/shop/files/Fronce-Abaya-made-with-crepe_2.webp?v=1759767647",
426
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe",
427
- "https://abayanoir.com/collections/accessories",
428
- "https://abayanoir.com/products/front-zip-elevated-cape-black",
429
- "https://abayanoir.com/search",
430
- "https://abayanoir.com/policies/shipping-policy",
431
- "https://abayanoir.com/collections/headwear",
432
- "https://abayanoir.com/collections/new",
433
- "https://abayanoir.com/collections/noir-originals",
434
- "https://abayanoir.com/cdn/shop/files/Fronce-Abaya-made-with-crepe_1.webp?v=1759767647",
435
- "https://abayanoir.com/cdn/shop/files/Fronce-Abaya-made-with-crepe_3.webp?v=1759767647",
436
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe#sizechart",
437
- "https://abayanoir.com/cart",
438
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe#ptabDesc",
439
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe#cartCoupon",
440
- "https://abayanoir.com/collections/basics",
441
- "https://abayanoir.com/collections/up-to-60-off",
442
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe#tabca4b9cc3-1de5-4957-9152-20d832cd0b41",
443
- "https://abayanoir.com/policies/privacy-policy",
444
- "https://abayanoir.com/",
445
- "https://abayanoir.com/collections/all",
446
- "https://abayanoir.com/collections/swimwear",
447
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe#cartNote",
448
- "https://abayanoir.com/cdn/shop/files/Fronce-Abaya-made-with-crepe_2_600x.webp?v=1759767647",
449
- "https://abayanoir.com/customer_authentication/redirect?locale=en®ion_country=EG",
450
- "https://abayanoir.com/collections/abaya",
451
- "https://abayanoir.com/collections/shirts-blouses",
452
- "https://abayanoir.com/cdn/shop/files/Cafe-wool-Jacket-4-pockets-2_29b2b685-428e-4324-a9e0-1a8c5fe83adb_600x.webp?v=1759767647",
453
- "https://abayanoir.com/cdn/shop/files/Fronce-Abaya-made-with-crepe_1_600x.webp?v=1759767647",
454
- "https://abayanoir.com/cdn/shop/files/Fronce-Abaya-made-with-crepe_3_600x.webp?v=1759767647",
455
- "https://abayanoir.com/collections/bottoms",
456
- "https://abayanoir.com/policies/refund-policy",
457
- "https://abayanoir.com/products/fronce-abaya-made-with-crepe#productInquiry",
458
- "https://abayanoir.com/collections/niqab",
459
- "https://abayanoir.com/cdn/shop/files/Cafe-wool-Jacket-4-pockets-2_29b2b685-428e-4324-a9e0-1a8c5fe83adb.webp?v=1759767647"
460
  ]
461
  }
462
  ],
463
  "audits": [
464
  {
465
- "url": "https://abayanoir.com/",
466
- "title": "Abaya Noir Your Basics & More – abayanoir1",
467
- "headings_ok": false,
468
- "density": {
469
- "avg_words": 4.764705882352941,
470
- "paras": 51
471
- },
472
- "entities": {
473
- "entities": [
474
- {
475
- "text": "Masaken Al Mohandesin",
476
- "label": "PERSON"
477
- },
478
- {
479
- "text": "Nasr City",
480
- "label": "GPE"
481
- },
482
- {
483
- "text": "Cairo",
484
- "label": "GPE"
485
- },
486
- {
487
- "text": "01289982763",
488
- "label": "DATE"
489
- },
490
- {
491
- "text": "2",
492
- "label": "CARDINAL"
493
- },
494
- {
495
- "text": "3rd Floor",
496
- "label": "ORG"
497
- },
498
- {
499
- "text": "3030",
500
- "label": "DATE"
501
- },
502
- {
503
- "text": "012",
504
- "label": "CARDINAL"
505
- },
506
- {
507
- "text": "7484",
508
- "label": "DATE"
509
- },
510
- {
511
- "text": "7469",
512
- "label": "DATE"
513
- },
514
- {
515
- "text": "october",
516
- "label": "DATE"
517
- },
518
- {
519
- "text": "012",
520
- "label": "CARDINAL"
521
- },
522
- {
523
- "text": "7468",
524
- "label": "DATE"
525
- },
526
- {
527
- "text": "Cairo",
528
- "label": "GPE"
529
- },
530
- {
531
- "text": "2nd",
532
- "label": "ORDINAL"
533
- },
534
- {
535
- "text": "012",
536
- "label": "CARDINAL"
537
- },
538
- {
539
- "text": "8998 2651",
540
- "label": "DATE"
541
- },
542
- {
543
- "text": "012",
544
- "label": "CARDINAL"
545
- },
546
- {
547
- "text": "8998 2789",
548
- "label": "DATE"
549
- },
550
- {
551
- "text": "Saudia",
552
- "label": "ORG"
553
- },
554
- {
555
- "text": "012",
556
- "label": "CARDINAL"
557
- },
558
- {
559
- "text": "8998 2763",
560
- "label": "DATE"
561
- },
562
- {
563
- "text": "AddressSan",
564
- "label": "GPE"
565
- },
566
- {
567
- "text": "3rd",
568
- "label": "ORDINAL"
569
- },
570
- {
571
- "text": "012",
572
- "label": "CARDINAL"
573
- },
574
- {
575
- "text": "8998 2794",
576
- "label": "DATE"
577
- },
578
- {
579
- "text": "Masaken Al Mohandesin",
580
- "label": "PERSON"
581
- },
582
- {
583
- "text": "Nasr City",
584
- "label": "GPE"
585
- },
586
- {
587
- "text": "Cairo",
588
- "label": "GPE"
589
- },
590
- {
591
- "text": "01289982763",
592
- "label": "DATE"
593
- },
594
- {
595
- "text": "2",
596
- "label": "CARDINAL"
597
- },
598
- {
599
- "text": "3rd Floor",
600
- "label": "ORG"
601
- },
602
- {
603
- "text": "3030",
604
- "label": "DATE"
605
- },
606
- {
607
- "text": "012",
608
- "label": "CARDINAL"
609
- },
610
- {
611
- "text": "7484",
612
- "label": "DATE"
613
- },
614
- {
615
- "text": "7469",
616
- "label": "DATE"
617
- },
618
- {
619
- "text": "october",
620
- "label": "DATE"
621
- },
622
- {
623
- "text": "012",
624
- "label": "CARDINAL"
625
- },
626
- {
627
- "text": "7468",
628
- "label": "DATE"
629
- },
630
- {
631
- "text": "Cairo",
632
- "label": "GPE"
633
- },
634
- {
635
- "text": "2nd",
636
- "label": "ORDINAL"
637
- },
638
- {
639
- "text": "012",
640
- "label": "CARDINAL"
641
- },
642
- {
643
- "text": "8998 2651",
644
- "label": "DATE"
645
- },
646
- {
647
- "text": "012",
648
- "label": "CARDINAL"
649
- },
650
- {
651
- "text": "8998 2789",
652
- "label": "DATE"
653
- },
654
- {
655
- "text": "Saudia",
656
- "label": "ORG"
657
- },
658
- {
659
- "text": "012",
660
- "label": "CARDINAL"
661
- },
662
- {
663
- "text": "8998 2763",
664
- "label": "DATE"
665
- },
666
- {
667
- "text": "AddressSan",
668
- "label": "GPE"
669
- },
670
- {
671
- "text": "3rd",
672
- "label": "ORDINAL"
673
- },
674
- {
675
- "text": "012",
676
- "label": "CARDINAL"
677
- },
678
- {
679
- "text": "8998 2794",
680
- "label": "DATE"
681
- },
682
- {
683
- "text": "100%",
684
- "label": "PERCENT"
685
- },
686
- {
687
- "text": "daily",
688
- "label": "DATE"
689
- },
690
- {
691
- "text": "20%",
692
- "label": "PERCENT"
693
- }
694
- ],
695
- "summary": {
696
- "PERSON": 2,
697
- "GPE": 8,
698
- "DATE": 21,
699
- "CARDINAL": 14,
700
- "ORG": 4,
701
- "ORDINAL": 4,
702
- "PERCENT": 2
703
- }
704
- }
705
- },
706
- {
707
- "url": "https://abayanoir.com/collections/best-sellers",
708
- "title": "Best Sellers – abayanoir1",
709
  "headings_ok": false,
710
  "density": {
711
- "avg_words": 4.416666666666667,
712
- "paras": 12
713
  },
714
  "entities": {
715
- "entities": [
716
- {
717
- "text": "daily",
718
- "label": "DATE"
719
- },
720
- {
721
- "text": "20%",
722
- "label": "PERCENT"
723
- }
724
- ],
725
- "summary": {
726
- "DATE": 1,
727
- "PERCENT": 1
728
- }
729
  }
730
  },
731
  {
732
- "url": "https://abayanoir.com/products/fronce-abaya-made-with-crepe",
733
- "title": "Fronce Abaya made with crepe abayanoir1",
734
  "headings_ok": false,
735
  "density": {
736
- "avg_words": 19.842105263157894,
737
- "paras": 19
738
  },
739
  "entities": {
740
- "entities": [
741
- {
742
- "text": "Wishlist",
743
- "label": "NORP"
744
- },
745
- {
746
- "text": "5",
747
- "label": "CARDINAL"
748
- },
749
- {
750
- "text": "7 working days",
751
- "label": "DATE"
752
- },
753
- {
754
- "text": "11:59 pm EST",
755
- "label": "TIME"
756
- },
757
- {
758
- "text": "4-5 days",
759
- "label": "DATE"
760
- },
761
- {
762
- "text": "4",
763
- "label": "CARDINAL"
764
- },
765
- {
766
- "text": "CO2",
767
- "label": "PRODUCT"
768
- },
769
- {
770
- "text": "an additional 5-6",
771
- "label": "CARDINAL"
772
- },
773
- {
774
- "text": "10",
775
- "label": "CARDINAL"
776
- },
777
- {
778
- "text": "Mohamed Kelani St",
779
- "label": "PERSON"
780
- },
781
- {
782
- "text": "Nozha St",
783
- "label": "FAC"
784
- },
785
- {
786
- "text": "Nasr city",
787
- "label": "GPE"
788
- },
789
- {
790
- "text": "cairo",
791
- "label": "GPE"
792
- },
793
- {
794
- "text": "Egyptand",
795
- "label": "GPE"
796
- },
797
- {
798
- "text": "14 days",
799
- "label": "DATE"
800
- },
801
- {
802
- "text": "14 days",
803
- "label": "DATE"
804
- },
805
- {
806
- "text": "daily",
807
- "label": "DATE"
808
- },
809
- {
810
- "text": "20%",
811
- "label": "PERCENT"
812
- },
813
- {
814
- "text": "Front-Zip Elevated Cape",
815
- "label": "ORG"
816
- }
817
- ],
818
- "summary": {
819
- "NORP": 1,
820
- "CARDINAL": 4,
821
- "DATE": 5,
822
- "TIME": 1,
823
- "PRODUCT": 1,
824
- "PERSON": 1,
825
- "FAC": 1,
826
- "GPE": 3,
827
- "PERCENT": 1,
828
- "ORG": 1
829
- }
830
  }
831
  }
832
  ],
@@ -834,14 +57,14 @@
834
  "enabled": true,
835
  "results": [
836
  {
837
- "query": "What is Abaya Noir – Your Basics & More – abayanoir1?",
838
  "error": "\n\nYou tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API.\n\nYou can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. \n\nAlternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28`\n\nA detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742\n"
839
  },
840
  {
841
- "query": "Best services for Abaya Noir – Your Basics & More – abayanoir1",
842
  "error": "\n\nYou tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API.\n\nYou can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. \n\nAlternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28`\n\nA detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742\n"
843
  }
844
  ]
845
  },
846
- "org_name": "Abaya Noir – Your Basics & More – abayanoir1"
847
  }
 
1
  {
2
  "pages": [
3
  {
4
+ "url": "https://rabhanagency.com/",
5
+ "title": "Rabhanagency - أفضل شركة تسويق في السعودية",
6
+ "headings": [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  "paragraphs": [
8
+ "Rabhanagency"
 
 
 
 
 
 
 
 
 
 
 
9
  ],
10
  "links": [
11
+ "https://rabhanagency.com/#content",
12
+ "https://rabhanagency.com/"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ]
14
  },
15
  {
16
+ "url": "https://rabhanagency.com/#content",
17
+ "title": "Rabhanagency - أفضل شركة تسويق في السعودية",
18
+ "headings": [],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  "paragraphs": [
20
+ "Rabhanagency"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
21
  ],
22
  "links": [
23
+ "https://rabhanagency.com/#content",
24
+ "https://rabhanagency.com/"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  ]
26
  }
27
  ],
28
  "audits": [
29
  {
30
+ "url": "https://rabhanagency.com/",
31
+ "title": "Rabhanagency - أفضل شركة تسويق في السعودية",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  "headings_ok": false,
33
  "density": {
34
+ "avg_words": 1.0,
35
+ "paras": 1
36
  },
37
  "entities": {
38
+ "entities": [],
39
+ "summary": {}
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
  },
42
  {
43
+ "url": "https://rabhanagency.com/#content",
44
+ "title": "Rabhanagency - أفضل شركة تسويق في السعودية",
45
  "headings_ok": false,
46
  "density": {
47
+ "avg_words": 1.0,
48
+ "paras": 1
49
  },
50
  "entities": {
51
+ "entities": [],
52
+ "summary": {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  }
54
  }
55
  ],
 
57
  "enabled": true,
58
  "results": [
59
  {
60
+ "query": "What is Rabhanagency?",
61
  "error": "\n\nYou tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API.\n\nYou can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. \n\nAlternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28`\n\nA detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742\n"
62
  },
63
  {
64
+ "query": "Best services for Rabhanagency",
65
  "error": "\n\nYou tried to access openai.ChatCompletion, but this is no longer supported in openai>=1.0.0 - see the README at https://github.com/openai/openai-python for the API.\n\nYou can run `openai migrate` to automatically upgrade your codebase to use the 1.0.0 interface. \n\nAlternatively, you can pin your installation to the old version, e.g. `pip install openai==0.28`\n\nA detailed migration guide is available here: https://github.com/openai/openai-python/discussions/742\n"
66
  }
67
  ]
68
  },
69
+ "org_name": "Rabhanagency"
70
  }
output/job-124/analysis.json CHANGED
@@ -6,7 +6,7 @@
6
  },
7
  "groq": {
8
  "enabled": true,
9
- "raw": "('id', 'chatcmpl-988c0548-89ab-4e71-a05e-6d61aefcbca0')('choices', [Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='```json\\n{\\n \"title\": \"Rabhanagency - أفضل شركة تسويق في السعودية\",\\n \"url\": \"https://rabhanagency.com/\",\\n \"text\": \"Rabhanagency\",\\n \"url_with_anchor\": \"https://rabhanagency.com/#content\",\\n \"text_duplicate\": \"Rabhanagency\"\\n}\\n```', role='assistant', annotations=None, executed_tools=None, function_call=None, reasoning=None, tool_calls=None))])('created', 1774269188)('model', 'llama-3.1-8b-instant')('object', 'chat.completion')('mcp_list_tools', None)('service_tier', 'on_demand')('system_fingerprint', 'fp_6a1eabf260')('usage', CompletionUsage(completion_tokens=74, prompt_tokens=106, total_tokens=180, completion_time=0.08283466, completion_tokens_details=None, prompt_time=0.005927158, prompt_tokens_details=None, queue_time=0.017948606, total_time=0.088761818))('usage_breakdown', None)('x_groq', XGroq(id='req_01kmdayhvhegvr3qtr2pr8cw45', debug=None, seed=1738281301, usage=None))",
10
  "parse_error": "No JSON found in LLM response"
11
  }
12
  },
@@ -26,72 +26,81 @@
26
  "passed": 0
27
  },
28
  "v2": {
29
- "score": 55.0,
30
  "breakdown": {
31
  "seo_rank": 100.0,
32
  "ai_visibility": 0.0,
33
- "traffic": 75.0
34
  },
35
  "avg_rank": 1.0
36
  }
37
  },
38
  "competitor_insight": {
39
- "monthly_visits": "75K",
40
  "traffic_sources": {
41
- "search": 45,
42
  "direct": 25,
43
- "social": 20,
44
- "referral": 10
45
  },
46
  "top_competitors": [
47
  {
48
  "name": "Rabhan",
49
  "domain": "rabhanagency.com",
50
- "overlap_score": 95,
51
  "region": "SA"
52
  },
53
  {
54
  "name": "Bayt.com",
55
  "domain": "bayt.com",
56
- "overlap_score": 88,
57
  "region": "Global"
58
  },
59
  {
60
- "name": "Souq.com",
61
- "domain": "souq.com",
62
- "overlap_score": 85,
63
  "region": "Global"
64
  }
65
  ],
66
  "regional_split": [
67
  {
68
  "country": "Saudi Arabia",
69
- "share": 60
70
  },
71
  {
72
  "country": "UAE",
73
- "share": 20
74
  },
75
  {
76
  "country": "Egypt",
77
  "share": 10
78
  },
79
  {
80
- "country": "Other MENA countries",
81
- "share": 10
 
 
 
 
82
  }
83
  ],
84
- "industry": "E-commerce and Digital Marketing",
85
  "seo_rankings": [
86
  {
87
- "query": "best e-commerce platforms in saudi arabia",
88
- "rank": 1,
89
- "link": "https://rabhanagency.com/best-e-commerce-platforms-in-saudi-arabia"
90
  },
91
  {
92
- "query": "top digital marketing agencies in saudi arabia",
93
- "rank": 2,
94
- "link": "https://rabhanagency.com/top-digital-marketing-agencies-in-saudi-arabia"
 
 
 
 
 
95
  }
96
  ]
97
  }
 
6
  },
7
  "groq": {
8
  "enabled": true,
9
+ "raw": "('id', 'chatcmpl-d3c9b14e-0736-492c-9d66-5660a68c3e6b')('choices', [Choice(finish_reason='stop', index=0, logprobs=None, message=ChatCompletionMessage(content='```json\\n{\\n \"title\": \"Rabhanagency - أفضل شركة تسويق في السعودية\",\\n \"url\": \"https://rabhanagency.com/\",\\n \"text\": \"Rabhanagency\"\\n}\\n```\\n\\n```json\\n{\\n \"title\": \"Rabhanagency - أفضل شركة تسويق في السعودية\",\\n \"url\": \"https://rabhanagency.com/#content\",\\n \"text\": \"Rabhanagency\"\\n}\\n```', role='assistant', annotations=None, executed_tools=None, function_call=None, reasoning=None, tool_calls=None))])('created', 1774271969)('model', 'llama-3.1-8b-instant')('object', 'chat.completion')('mcp_list_tools', None)('service_tier', 'on_demand')('system_fingerprint', 'fp_8639719ff2')('usage', CompletionUsage(completion_tokens=96, prompt_tokens=106, total_tokens=202, completion_time=0.094449388, completion_tokens_details=None, prompt_time=0.006478208, prompt_tokens_details=None, queue_time=0.018456577, total_time=0.100927596))('usage_breakdown', None)('x_groq', XGroq(id='req_01kmddke09ftsrxewc3g14fwh6', debug=None, seed=2120619979, usage=None))",
10
  "parse_error": "No JSON found in LLM response"
11
  }
12
  },
 
26
  "passed": 0
27
  },
28
  "v2": {
29
+ "score": 60.0,
30
  "breakdown": {
31
  "seo_rank": 100.0,
32
  "ai_visibility": 0.0,
33
+ "traffic": 100
34
  },
35
  "avg_rank": 1.0
36
  }
37
  },
38
  "competitor_insight": {
39
+ "monthly_visits": "150K",
40
  "traffic_sources": {
41
+ "search": 55,
42
  "direct": 25,
43
+ "social": 15,
44
+ "referral": 5
45
  },
46
  "top_competitors": [
47
  {
48
  "name": "Rabhan",
49
  "domain": "rabhanagency.com",
50
+ "overlap_score": 92,
51
  "region": "SA"
52
  },
53
  {
54
  "name": "Bayt.com",
55
  "domain": "bayt.com",
56
+ "overlap_score": 85,
57
  "region": "Global"
58
  },
59
  {
60
+ "name": "Savvi",
61
+ "domain": "savvi.com",
62
+ "overlap_score": 78,
63
  "region": "Global"
64
  }
65
  ],
66
  "regional_split": [
67
  {
68
  "country": "Saudi Arabia",
69
+ "share": 55
70
  },
71
  {
72
  "country": "UAE",
73
+ "share": 25
74
  },
75
  {
76
  "country": "Egypt",
77
  "share": 10
78
  },
79
  {
80
+ "country": "Kuwait",
81
+ "share": 5
82
+ },
83
+ {
84
+ "country": "Oman",
85
+ "share": 5
86
  }
87
  ],
88
+ "industry": "Digital Marketing and E-commerce",
89
  "seo_rankings": [
90
  {
91
+ "query": "best marketing agencies in saudi arabia",
92
+ "rank": 2,
93
+ "link": "https://rabhanagency.com/"
94
  },
95
  {
96
+ "query": "top advertising agencies in saudi arabia",
97
+ "rank": 3,
98
+ "link": "https://rabhanagency.com/"
99
+ },
100
+ {
101
+ "query": "best e-commerce platforms in saudi arabia",
102
+ "rank": 5,
103
+ "link": "https://rabhanagency.com/"
104
  }
105
  ]
106
  }
output/job-28/audit.json CHANGED
@@ -453,15 +453,15 @@
453
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
454
  ],
455
  "links": [
456
- "https://mohrek.com/wp-content/uploads/2024/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
457
  "https://mohrek.com",
458
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
459
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
460
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
461
- "https://mohrek.com/wp-content/uploads/2024/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
462
  "https://mohrek.com/%d8%a7%d9%86%d8%b4%d8%a7%d8%a1-%d8%a7%d9%84%d9%85%d9%88%d8%a7%d9%82%d8%b9-%d9%88%d8%a7%d9%84%d9%85%d8%aa%d8%a7%d8%ac%d8%b1-%d8%a7%d9%84%d8%a5%d9%84%d9%83%d8%aa%d8%b1%d9%88%d9%86%d9%8a%d8%a9/",
463
  "https://mohrek.com/?page_id=16015",
464
- "https://mohrek.com/wp-content/uploads/2024/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
465
  "https://mohrek.com/",
466
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
467
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
@@ -471,9 +471,9 @@
471
  "https://mohrek.com/contact/",
472
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
473
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
474
- "https://mohrek.com/wp-content/uploads/2024/02/2626.png",
475
- "https://mohrek.com/wp-content/uploads/2024/02/Asset-31.png",
476
- "https://mohrek.com/wp-content/uploads/2024/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
477
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf"
478
  ]
479
  },
 
453
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
454
  ],
455
  "links": [
456
+ "https://mohrek.com/wp-content/uploads/2026/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
457
  "https://mohrek.com",
458
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
459
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
460
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
461
+ "https://mohrek.com/wp-content/uploads/2026/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
462
  "https://mohrek.com/%d8%a7%d9%86%d8%b4%d8%a7%d8%a1-%d8%a7%d9%84%d9%85%d9%88%d8%a7%d9%82%d8%b9-%d9%88%d8%a7%d9%84%d9%85%d8%aa%d8%a7%d8%ac%d8%b1-%d8%a7%d9%84%d8%a5%d9%84%d9%83%d8%aa%d8%b1%d9%88%d9%86%d9%8a%d8%a9/",
463
  "https://mohrek.com/?page_id=16015",
464
+ "https://mohrek.com/wp-content/uploads/2026/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
465
  "https://mohrek.com/",
466
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
467
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
 
471
  "https://mohrek.com/contact/",
472
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
473
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
474
+ "https://mohrek.com/wp-content/uploads/2026/02/2626.png",
475
+ "https://mohrek.com/wp-content/uploads/2026/02/Asset-31.png",
476
+ "https://mohrek.com/wp-content/uploads/2026/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
477
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf"
478
  ]
479
  },
output/job-87/audit.json CHANGED
@@ -636,28 +636,28 @@
636
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
637
  ],
638
  "links": [
639
- "https://mohrek.com/wp-content/uploads/2024/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
640
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/",
641
  "https://mohrek.com",
642
- "https://mohrek.com/wp-content/uploads/2024/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
643
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
644
  "https://mohrek.com/contact/",
645
- "https://mohrek.com/wp-content/uploads/2024/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
646
  "https://mohrek.com/%d8%b3%d8%a7%d8%a8%d9%82%d8%a9-%d8%a3%d8%b9%d9%85%d8%a7%d9%84-%d9%85%d8%ad%d8%b1%d9%83/",
647
- "https://mohrek.com/wp-content/uploads/2024/02/2626.png",
648
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
649
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
650
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-seo-%d8%a7%d9%84%d9%8a%d9%88%d8%aa%d9%8a%d9%88%d8%a8/",
651
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
652
  "https://mohrek.com/?page_id=16015",
653
  "https://mohrek.com/",
654
- "https://mohrek.com/wp-content/uploads/2024/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
655
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
656
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
657
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
658
  "https://mohrek.com/%d8%a7%d9%86%d8%b4%d8%a7%d8%a1-%d8%a7%d9%84%d9%85%d9%88%d8%a7%d9%82%d8%b9-%d9%88%d8%a7%d9%84%d9%85%d8%aa%d8%a7%d8%ac%d8%b1-%d8%a7%d9%84%d8%a5%d9%84%d9%83%d8%aa%d8%b1%d9%88%d9%86%d9%8a%d8%a9/",
659
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf",
660
- "https://mohrek.com/wp-content/uploads/2024/02/Asset-31.png"
661
  ]
662
  }
663
  ],
 
636
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
637
  ],
638
  "links": [
639
+ "https://mohrek.com/wp-content/uploads/2026/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
640
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/",
641
  "https://mohrek.com",
642
+ "https://mohrek.com/wp-content/uploads/2026/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
643
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
644
  "https://mohrek.com/contact/",
645
+ "https://mohrek.com/wp-content/uploads/2026/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
646
  "https://mohrek.com/%d8%b3%d8%a7%d8%a8%d9%82%d8%a9-%d8%a3%d8%b9%d9%85%d8%a7%d9%84-%d9%85%d8%ad%d8%b1%d9%83/",
647
+ "https://mohrek.com/wp-content/uploads/2026/02/2626.png",
648
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
649
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
650
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-seo-%d8%a7%d9%84%d9%8a%d9%88%d8%aa%d9%8a%d9%88%d8%a8/",
651
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
652
  "https://mohrek.com/?page_id=16015",
653
  "https://mohrek.com/",
654
+ "https://mohrek.com/wp-content/uploads/2026/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
655
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
656
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
657
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
658
  "https://mohrek.com/%d8%a7%d9%86%d8%b4%d8%a7%d8%a1-%d8%a7%d9%84%d9%85%d9%88%d8%a7%d9%82%d8%b9-%d9%88%d8%a7%d9%84%d9%85%d8%aa%d8%a7%d8%ac%d8%b1-%d8%a7%d9%84%d8%a5%d9%84%d9%83%d8%aa%d8%b1%d9%88%d9%86%d9%8a%d8%a9/",
659
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf",
660
+ "https://mohrek.com/wp-content/uploads/2026/02/Asset-31.png"
661
  ]
662
  }
663
  ],
output/job-96/audit.json CHANGED
@@ -453,17 +453,17 @@
453
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
454
  ],
455
  "links": [
456
- "https://mohrek.com/wp-content/uploads/2024/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
457
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-seo-%d8%a7%d9%84%d9%8a%d9%88%d8%aa%d9%8a%d9%88%d8%a8/",
458
- "https://mohrek.com/wp-content/uploads/2024/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
459
  "https://mohrek.com",
460
  "https://mohrek.com/",
461
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/",
462
  "https://mohrek.com/contact/",
463
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
464
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
465
- "https://mohrek.com/wp-content/uploads/2024/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
466
- "https://mohrek.com/wp-content/uploads/2024/02/Asset-31.png",
467
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
468
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
469
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf",
@@ -472,9 +472,9 @@
472
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
473
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
474
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
475
- "https://mohrek.com/wp-content/uploads/2024/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
476
  "https://mohrek.com/?page_id=16015",
477
- "https://mohrek.com/wp-content/uploads/2024/02/2626.png"
478
  ]
479
  },
480
  {
 
453
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
454
  ],
455
  "links": [
456
+ "https://mohrek.com/wp-content/uploads/2026/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
457
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-seo-%d8%a7%d9%84%d9%8a%d9%88%d8%aa%d9%8a%d9%88%d8%a8/",
458
+ "https://mohrek.com/wp-content/uploads/2026/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
459
  "https://mohrek.com",
460
  "https://mohrek.com/",
461
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/",
462
  "https://mohrek.com/contact/",
463
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
464
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
465
+ "https://mohrek.com/wp-content/uploads/2026/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
466
+ "https://mohrek.com/wp-content/uploads/2026/02/Asset-31.png",
467
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
468
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
469
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf",
 
472
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
473
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
474
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
475
+ "https://mohrek.com/wp-content/uploads/2026/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
476
  "https://mohrek.com/?page_id=16015",
477
+ "https://mohrek.com/wp-content/uploads/2026/02/2626.png"
478
  ]
479
  },
480
  {
output/job-97/audit.json CHANGED
@@ -453,17 +453,17 @@
453
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
454
  ],
455
  "links": [
456
- "https://mohrek.com/wp-content/uploads/2024/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
457
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-seo-%d8%a7%d9%84%d9%8a%d9%88%d8%aa%d9%8a%d9%88%d8%a8/",
458
- "https://mohrek.com/wp-content/uploads/2024/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
459
  "https://mohrek.com",
460
  "https://mohrek.com/",
461
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/",
462
  "https://mohrek.com/contact/",
463
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
464
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
465
- "https://mohrek.com/wp-content/uploads/2024/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
466
- "https://mohrek.com/wp-content/uploads/2024/02/Asset-31.png",
467
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
468
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
469
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf",
@@ -472,9 +472,9 @@
472
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
473
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
474
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
475
- "https://mohrek.com/wp-content/uploads/2024/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
476
  "https://mohrek.com/?page_id=16015",
477
- "https://mohrek.com/wp-content/uploads/2024/02/2626.png"
478
  ]
479
  },
480
  {
 
453
  "استقطب آلاف العملاء الحقيقيين الى موقعك الإلكتروني من خلال خطط محرك المتوازية مع أهداف نشاطك التجاري."
454
  ],
455
  "links": [
456
+ "https://mohrek.com/wp-content/uploads/2026/02/1OcxXdFttB0mNKzhFdghlyJn49zTDQ2PfkkBlGpX.png",
457
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-seo-%d8%a7%d9%84%d9%8a%d9%88%d8%aa%d9%8a%d9%88%d8%a8/",
458
+ "https://mohrek.com/wp-content/uploads/2026/02/cwfPBg9Z062cS6nXBwWh4KleqlMsYXk2I4DsWKc5.png",
459
  "https://mohrek.com",
460
  "https://mohrek.com/",
461
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/",
462
  "https://mohrek.com/contact/",
463
  "https://mohrek.com/%d8%a5%d8%b9%d9%84%d8%a7%d9%86%d8%a7%d8%aa-%d8%ac%d9%88%d8%ac%d9%84/",
464
  "https://mohrek.com/%d8%a7%d9%84%d9%85%d8%af%d9%88%d9%86%d8%a9-2/",
465
+ "https://mohrek.com/wp-content/uploads/2026/02/b82ce427d88938848bf2e6202ca39d7b_4dbd0000-151b-443a-b504-336c52fd9490.png",
466
+ "https://mohrek.com/wp-content/uploads/2026/02/Asset-31.png",
467
  "https://mohrek.com/seo-%d8%a7%d9%84%d8%b0%d9%83%d8%a7%d8%a1-%d8%a7%d9%84%d8%a7%d8%b5%d8%b7%d9%86%d8%a7%d8%b9%d9%8a/",
468
  "https://mohrek.com/%d9%85%d9%86-%d9%86%d8%ad%d9%86-%d9%85%d8%ad%d8%b1%d9%83/",
469
  "https://mohrek.com/wp-content/uploads/2025/08/Certificate-12.pdf",
 
472
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d9%85%d8%ad%d8%b1%d9%83%d8%a7%d8%aa-%d8%a7%d9%84%d8%a8%d8%ad%d8%ab-seo-%d9%85%d9%86-%d8%b4%d8%b1%d9%83%d8%a9-%d9%85%d8%ad%d8%b1%d9%83/#content",
473
  "https://mohrek.com/%d8%aa%d8%ad%d8%b3%d9%8a%d9%86-%d8%a8%d8%ad%d8%ab-%d8%a7%d9%84%d8%aa%d8%b7%d8%a8%d9%8a%d9%82%d8%a7%d8%aa-aso/",
474
  "https://mohrek.com/%d8%ae%d8%af%d9%85%d8%a7%d8%aa-%d9%85%d8%ad%d8%b1%d9%83/",
475
+ "https://mohrek.com/wp-content/uploads/2026/02/kMNIaBy2VFJpC2kyIl4wzi9qqrz64Toi8q4DnLk8.png",
476
  "https://mohrek.com/?page_id=16015",
477
+ "https://mohrek.com/wp-content/uploads/2026/02/2626.png"
478
  ]
479
  },
480
  {
server/advanced_features.py ADDED
@@ -0,0 +1,407 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ GEO Platform — Advanced Features Module
3
+ Covers: keyword tracking, scheduled crawls, smart alerts, email reports,
4
+ competitor gap analysis, bulk URL analysis.
5
+ """
6
+ import os
7
+ import json
8
+ import sqlite3
9
+ import smtplib
10
+ import threading
11
+ from datetime import datetime, timedelta
12
+ from pathlib import Path
13
+ from email.mime.text import MIMEText
14
+ from email.mime.multipart import MIMEMultipart
15
+ from typing import List, Dict, Optional
16
+
17
+ _OUTPUT = Path(os.environ.get('OUTPUT_DIR', Path(__file__).resolve().parent.parent / 'output'))
18
+ _OUTPUT.mkdir(parents=True, exist_ok=True)
19
+ DB_PATH = _OUTPUT / 'jobs.db'
20
+
21
+
22
+ # ── DB helpers ────────────────────────────────────────────────────────────────
23
+
24
+ def _conn():
25
+ c = sqlite3.connect(str(DB_PATH), detect_types=sqlite3.PARSE_DECLTYPES)
26
+ c.row_factory = sqlite3.Row
27
+ return c
28
+
29
+
30
+ def init_advanced_tables():
31
+ """Create tables for keyword tracking, alerts, and scheduled jobs."""
32
+ c = _conn()
33
+ cur = c.cursor()
34
+
35
+ # Keyword history
36
+ cur.execute('''CREATE TABLE IF NOT EXISTS keyword_history (
37
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
38
+ job_id INTEGER,
39
+ url TEXT,
40
+ keyword TEXT,
41
+ count INTEGER,
42
+ volume INTEGER,
43
+ cpc REAL,
44
+ competition TEXT,
45
+ opportunity_score INTEGER,
46
+ tracked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
47
+ )''')
48
+
49
+ # GEO score history
50
+ cur.execute('''CREATE TABLE IF NOT EXISTS geo_score_history (
51
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
52
+ job_id INTEGER,
53
+ url TEXT,
54
+ score INTEGER,
55
+ status TEXT,
56
+ breakdown TEXT,
57
+ tracked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
58
+ )''')
59
+
60
+ # Smart alerts
61
+ cur.execute('''CREATE TABLE IF NOT EXISTS alerts (
62
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
63
+ url TEXT,
64
+ alert_type TEXT,
65
+ message TEXT,
66
+ severity TEXT,
67
+ seen INTEGER DEFAULT 0,
68
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
69
+ )''')
70
+
71
+ # Scheduled crawls
72
+ cur.execute('''CREATE TABLE IF NOT EXISTS scheduled_crawls (
73
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
74
+ url TEXT,
75
+ org_name TEXT,
76
+ org_url TEXT,
77
+ max_pages INTEGER DEFAULT 3,
78
+ frequency TEXT DEFAULT 'weekly',
79
+ next_run TIMESTAMP,
80
+ last_run TIMESTAMP,
81
+ active INTEGER DEFAULT 1,
82
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
83
+ )''')
84
+
85
+ c.commit()
86
+ c.close()
87
+
88
+
89
+ # ── Keyword Tracking ──────────────────────────────────────────────────────────
90
+
91
+ def save_keyword_snapshot(job_id: int, url: str, keywords: List[Dict]):
92
+ """Save keyword data snapshot for trend tracking."""
93
+ init_advanced_tables()
94
+ c = _conn()
95
+ cur = c.cursor()
96
+ now = datetime.utcnow()
97
+ for kw in keywords[:50]:
98
+ cur.execute('''INSERT INTO keyword_history
99
+ (job_id, url, keyword, count, volume, cpc, competition, opportunity_score, tracked_at)
100
+ VALUES (?,?,?,?,?,?,?,?,?)''', (
101
+ job_id, url,
102
+ kw.get('kw', ''),
103
+ kw.get('count', 0),
104
+ kw.get('volume'),
105
+ kw.get('cpc'),
106
+ kw.get('competition'),
107
+ kw.get('opportunity_score', 0),
108
+ now
109
+ ))
110
+ c.commit()
111
+ c.close()
112
+
113
+
114
+ def save_geo_score_snapshot(job_id: int, url: str, geo_score: Dict):
115
+ """Save GEO score for trend tracking."""
116
+ init_advanced_tables()
117
+ c = _conn()
118
+ cur = c.cursor()
119
+ cur.execute('''INSERT INTO geo_score_history
120
+ (job_id, url, score, status, breakdown, tracked_at)
121
+ VALUES (?,?,?,?,?,?)''', (
122
+ job_id, url,
123
+ geo_score.get('score', 0),
124
+ geo_score.get('status', ''),
125
+ json.dumps(geo_score.get('breakdown', {})),
126
+ datetime.utcnow()
127
+ ))
128
+ c.commit()
129
+ c.close()
130
+
131
+
132
+ def get_keyword_trends(url: str, keyword: str = None, days: int = 30) -> List[Dict]:
133
+ """Get keyword history for a URL over time."""
134
+ init_advanced_tables()
135
+ c = _conn()
136
+ cur = c.cursor()
137
+ since = datetime.utcnow() - timedelta(days=days)
138
+ if keyword:
139
+ cur.execute('''SELECT * FROM keyword_history
140
+ WHERE url=? AND keyword=? AND tracked_at > ?
141
+ ORDER BY tracked_at ASC''', (url, keyword, since))
142
+ else:
143
+ cur.execute('''SELECT keyword, MAX(volume) as volume, MAX(opportunity_score) as opp,
144
+ COUNT(*) as snapshots, MAX(tracked_at) as last_seen
145
+ FROM keyword_history WHERE url=? AND tracked_at > ?
146
+ GROUP BY keyword ORDER BY opp DESC LIMIT 30''', (url, since))
147
+ rows = [dict(r) for r in cur.fetchall()]
148
+ c.close()
149
+ return rows
150
+
151
+
152
+ def get_geo_score_trends(url: str, days: int = 90) -> List[Dict]:
153
+ """Get GEO score history for trend chart."""
154
+ init_advanced_tables()
155
+ c = _conn()
156
+ cur = c.cursor()
157
+ since = datetime.utcnow() - timedelta(days=days)
158
+ cur.execute('''SELECT score, status, breakdown, tracked_at FROM geo_score_history
159
+ WHERE url=? AND tracked_at > ? ORDER BY tracked_at ASC''', (url, since))
160
+ rows = [dict(r) for r in cur.fetchall()]
161
+ c.close()
162
+ return rows
163
+
164
+
165
+ # ── Smart Alerts ──────────────────────────────────────────────────────────────
166
+
167
+ def check_and_create_alerts(job_id: int, url: str, geo_score: Dict, prev_score: Optional[int] = None):
168
+ """Automatically create alerts based on GEO score changes."""
169
+ init_advanced_tables()
170
+ alerts = []
171
+ score = geo_score.get('score', 0)
172
+
173
+ # Score drop alert
174
+ if prev_score is not None and score < prev_score - 10:
175
+ alerts.append({
176
+ 'url': url, 'alert_type': 'score_drop',
177
+ 'message': f'انخفضت درجة GEO من {prev_score}% إلى {score}% — تراجع {prev_score - score} نقطة',
178
+ 'severity': 'high'
179
+ })
180
+
181
+ # Critical score alert
182
+ if score < 30:
183
+ alerts.append({
184
+ 'url': url, 'alert_type': 'critical_score',
185
+ 'message': f'درجة GEO حرجة: {score}% — الموقع غير مرئي لمحركات الذكاء الاصطناعي',
186
+ 'severity': 'critical'
187
+ })
188
+
189
+ # Missing H1 alert
190
+ breakdown = geo_score.get('breakdown', {})
191
+ if breakdown.get('headings', 20) < 5:
192
+ alerts.append({
193
+ 'url': url, 'alert_type': 'missing_h1',
194
+ 'message': 'عناوين H1 مفقودة أو ضعيفة — يؤثر على الفهرسة',
195
+ 'severity': 'medium'
196
+ })
197
+
198
+ # No AI visibility
199
+ if breakdown.get('ai_visibility', 20) < 5:
200
+ alerts.append({
201
+ 'url': url, 'alert_type': 'no_ai_visibility',
202
+ 'message': 'لا يوجد ظهور في محركات الذكاء الاصطناعي — العلامة التجارية غير معروفة',
203
+ 'severity': 'high'
204
+ })
205
+
206
+ if alerts:
207
+ c = _conn()
208
+ cur = c.cursor()
209
+ for a in alerts:
210
+ cur.execute('''INSERT INTO alerts (url, alert_type, message, severity)
211
+ VALUES (?,?,?,?)''', (a['url'], a['alert_type'], a['message'], a['severity']))
212
+ c.commit()
213
+ c.close()
214
+
215
+ return alerts
216
+
217
+
218
+ def get_alerts(url: str = None, unseen_only: bool = False) -> List[Dict]:
219
+ """Get alerts, optionally filtered by URL or unseen status."""
220
+ init_advanced_tables()
221
+ c = _conn()
222
+ cur = c.cursor()
223
+ query = 'SELECT * FROM alerts WHERE 1=1'
224
+ params = []
225
+ if url:
226
+ query += ' AND url=?'; params.append(url)
227
+ if unseen_only:
228
+ query += ' AND seen=0'
229
+ query += ' ORDER BY created_at DESC LIMIT 50'
230
+ cur.execute(query, params)
231
+ rows = [dict(r) for r in cur.fetchall()]
232
+ c.close()
233
+ return rows
234
+
235
+
236
+ def mark_alerts_seen(alert_ids: List[int]):
237
+ c = _conn()
238
+ c.execute(f'UPDATE alerts SET seen=1 WHERE id IN ({",".join("?" * len(alert_ids))})', alert_ids)
239
+ c.commit()
240
+ c.close()
241
+
242
+
243
+ # ── Scheduled Crawls ──────────────────────────────────────────────────────────
244
+
245
+ def add_scheduled_crawl(url: str, org_name: str, org_url: str,
246
+ max_pages: int = 3, frequency: str = 'weekly') -> int:
247
+ """Schedule a recurring crawl. frequency: daily | weekly | monthly"""
248
+ init_advanced_tables()
249
+ freq_map = {'daily': 1, 'weekly': 7, 'monthly': 30}
250
+ days = freq_map.get(frequency, 7)
251
+ next_run = datetime.utcnow() + timedelta(days=days)
252
+ c = _conn()
253
+ cur = c.cursor()
254
+ cur.execute('''INSERT INTO scheduled_crawls
255
+ (url, org_name, org_url, max_pages, frequency, next_run, active)
256
+ VALUES (?,?,?,?,?,?,1)''', (url, org_name, org_url, max_pages, frequency, next_run))
257
+ sid = cur.lastrowid
258
+ c.commit()
259
+ c.close()
260
+ return sid
261
+
262
+
263
+ def list_scheduled_crawls() -> List[Dict]:
264
+ init_advanced_tables()
265
+ c = _conn()
266
+ cur = c.cursor()
267
+ cur.execute('SELECT * FROM scheduled_crawls ORDER BY next_run ASC')
268
+ rows = [dict(r) for r in cur.fetchall()]
269
+ c.close()
270
+ return rows
271
+
272
+
273
+ def delete_scheduled_crawl(schedule_id: int):
274
+ c = _conn()
275
+ c.execute('DELETE FROM scheduled_crawls WHERE id=?', (schedule_id,))
276
+ c.commit()
277
+ c.close()
278
+
279
+
280
+ def run_due_scheduled_crawls():
281
+ """Called by scheduler — enqueues jobs for due scheduled crawls."""
282
+ try:
283
+ from server import job_queue
284
+ init_advanced_tables()
285
+ c = _conn()
286
+ cur = c.cursor()
287
+ now = datetime.utcnow()
288
+ cur.execute('SELECT * FROM scheduled_crawls WHERE active=1 AND next_run <= ?', (now,))
289
+ due = [dict(r) for r in cur.fetchall()]
290
+
291
+ for s in due:
292
+ # Enqueue the job
293
+ job_queue.enqueue_job(s['url'], s['org_name'], s['org_url'], s['max_pages'])
294
+ # Update next_run
295
+ freq_map = {'daily': 1, 'weekly': 7, 'monthly': 30}
296
+ days = freq_map.get(s['frequency'], 7)
297
+ next_run = now + timedelta(days=days)
298
+ cur.execute('UPDATE scheduled_crawls SET last_run=?, next_run=? WHERE id=?',
299
+ (now, next_run, s['id']))
300
+
301
+ c.commit()
302
+ c.close()
303
+ except Exception as e:
304
+ print(f'Scheduler error: {e}')
305
+
306
+
307
+ def start_scheduler():
308
+ """Start background scheduler thread."""
309
+ def _loop():
310
+ import time
311
+ while True:
312
+ run_due_scheduled_crawls()
313
+ time.sleep(3600) # check every hour
314
+ t = threading.Thread(target=_loop, daemon=True)
315
+ t.start()
316
+
317
+
318
+ # ── Competitor Gap Analysis ───────────────────────────────────────────────────
319
+
320
+ def competitor_keyword_gap(your_keywords: List[str], competitor_url: str,
321
+ max_pages: int = 3) -> Dict:
322
+ """Find keywords competitor has that you don't."""
323
+ try:
324
+ from src.crawler import crawl_seed
325
+ from server.keyword_engine import extract_keywords_from_audit
326
+
327
+ pages = crawl_seed(competitor_url, max_pages=max_pages)
328
+ audit = {'pages': pages}
329
+ comp_kws = extract_keywords_from_audit(audit, top_n=50, enrich=False)
330
+ comp_set = {k.get('kw', '').lower() for k in comp_kws}
331
+ your_set = {k.lower() for k in your_keywords}
332
+
333
+ gaps = comp_set - your_set
334
+ overlaps = comp_set & your_set
335
+
336
+ return {
337
+ 'competitor_url': competitor_url,
338
+ 'competitor_keywords': len(comp_set),
339
+ 'your_keywords': len(your_set),
340
+ 'gaps': sorted(list(gaps))[:30],
341
+ 'overlaps': sorted(list(overlaps))[:20],
342
+ 'gap_count': len(gaps),
343
+ 'opportunity_score': min(100, int((len(gaps) / max(len(comp_set), 1)) * 100))
344
+ }
345
+ except Exception as e:
346
+ return {'error': str(e), 'gaps': [], 'gap_count': 0}
347
+
348
+
349
+ # ── Bulk URL Analysis ─────────────────────────────────────────────────────────
350
+
351
+ def bulk_enqueue(urls: List[str], org_name: str = '', max_pages: int = 2) -> List[int]:
352
+ """Enqueue multiple URLs as separate jobs."""
353
+ from server import job_queue
354
+ job_ids = []
355
+ for url in urls[:10]: # max 10 at once
356
+ url = url.strip()
357
+ if not url:
358
+ continue
359
+ jid = job_queue.enqueue_job(url, org_name or url, url, max_pages)
360
+ job_ids.append(jid)
361
+ return job_ids
362
+
363
+
364
+ # ── Email Reports ─────────────────────────────────────────────────────────────
365
+
366
+ def send_email_report(to_email: str, subject: str, html_body: str,
367
+ smtp_host: str = None, smtp_port: int = 587,
368
+ smtp_user: str = None, smtp_pass: str = None) -> bool:
369
+ """Send HTML email report via SMTP."""
370
+ smtp_host = smtp_host or os.getenv('SMTP_HOST', '')
371
+ smtp_user = smtp_user or os.getenv('SMTP_USER', '')
372
+ smtp_pass = smtp_pass or os.getenv('SMTP_PASS', '')
373
+
374
+ if not smtp_host or not smtp_user:
375
+ return False
376
+
377
+ try:
378
+ msg = MIMEMultipart('alternative')
379
+ msg['Subject'] = subject
380
+ msg['From'] = smtp_user
381
+ msg['To'] = to_email
382
+ msg.attach(MIMEText(html_body, 'html', 'utf-8'))
383
+
384
+ with smtplib.SMTP(smtp_host, smtp_port) as server:
385
+ server.starttls()
386
+ server.login(smtp_user, smtp_pass)
387
+ server.sendmail(smtp_user, to_email, msg.as_string())
388
+ return True
389
+ except Exception as e:
390
+ print(f'Email error: {e}')
391
+ return False
392
+
393
+
394
+ def send_weekly_report(to_email: str, url: str):
395
+ """Build and send a weekly GEO report for a URL."""
396
+ from server.reports import build_html_report
397
+ from server import job_queue
398
+
399
+ # Find latest completed job for this URL
400
+ jobs = job_queue.list_jobs(limit=100)
401
+ job = next((j for j in jobs if j.get('url') == url and j.get('status') == 'completed'), None)
402
+ if not job or not job.get('result_path'):
403
+ return False
404
+
405
+ html = build_html_report(job['result_path'])
406
+ subject = f'تقرير GEO الأسبوعي — {url}'
407
+ return send_email_report(to_email, subject, html)
server/api.py CHANGED
@@ -1333,3 +1333,213 @@ async def serve_regional_dashboard():
1333
  async def serve_ads_dashboard():
1334
  """Serve the Paid Ads Management dashboard."""
1335
  return FileResponse(str(frontend_dir / 'ads.html'))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1333
  async def serve_ads_dashboard():
1334
  """Serve the Paid Ads Management dashboard."""
1335
  return FileResponse(str(frontend_dir / 'ads.html'))
1336
+
1337
+
1338
+ # ═══════════════════════════════════════════════════════════════════════════════
1339
+ # ADVANCED FEATURES — Keyword Tracking, Alerts, Scheduler, Bulk, Gap, Email
1340
+ # ═══════════════════════════════════════════════════════════════════════════════
1341
+
1342
+ from server.advanced_features import (
1343
+ save_keyword_snapshot, save_geo_score_snapshot,
1344
+ get_keyword_trends, get_geo_score_trends,
1345
+ check_and_create_alerts, get_alerts, mark_alerts_seen,
1346
+ add_scheduled_crawl, list_scheduled_crawls, delete_scheduled_crawl,
1347
+ competitor_keyword_gap, bulk_enqueue, send_weekly_report,
1348
+ start_scheduler, init_advanced_tables
1349
+ )
1350
+
1351
+ try:
1352
+ init_advanced_tables()
1353
+ start_scheduler()
1354
+ except Exception:
1355
+ pass
1356
+
1357
+
1358
+ @app.get('/api/tracking/keywords')
1359
+ async def api_keyword_trends(url: str, keyword: str = None, days: int = 30):
1360
+ try:
1361
+ return {'ok': True, 'trends': get_keyword_trends(url, keyword, days)}
1362
+ except Exception as e:
1363
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1364
+
1365
+
1366
+ @app.get('/api/tracking/geo-score')
1367
+ async def api_geo_score_trends(url: str, days: int = 90):
1368
+ try:
1369
+ return {'ok': True, 'trends': get_geo_score_trends(url, days)}
1370
+ except Exception as e:
1371
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1372
+
1373
+
1374
+ @app.get('/api/alerts')
1375
+ async def api_get_alerts(url: str = None, unseen_only: bool = False):
1376
+ try:
1377
+ alerts = get_alerts(url, unseen_only)
1378
+ return {'ok': True, 'alerts': alerts, 'count': len(alerts)}
1379
+ except Exception as e:
1380
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1381
+
1382
+
1383
+ @app.post('/api/alerts/seen')
1384
+ async def api_mark_alerts_seen(req: dict):
1385
+ try:
1386
+ mark_alerts_seen(req.get('ids', []))
1387
+ return {'ok': True}
1388
+ except Exception as e:
1389
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1390
+
1391
+
1392
+ class ScheduleRequest(BaseModel):
1393
+ url: str
1394
+ org_name: str = ''
1395
+ org_url: str = ''
1396
+ max_pages: int = 3
1397
+ frequency: str = 'weekly'
1398
+
1399
+
1400
+ @app.post('/api/schedule')
1401
+ async def api_add_schedule(req: ScheduleRequest):
1402
+ try:
1403
+ sid = add_scheduled_crawl(req.url, req.org_name, req.org_url, req.max_pages, req.frequency)
1404
+ return {'ok': True, 'schedule_id': sid}
1405
+ except Exception as e:
1406
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1407
+
1408
+
1409
+ @app.get('/api/schedule')
1410
+ async def api_list_schedules():
1411
+ try:
1412
+ return {'ok': True, 'schedules': list_scheduled_crawls()}
1413
+ except Exception as e:
1414
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1415
+
1416
+
1417
+ @app.delete('/api/schedule/{schedule_id}')
1418
+ async def api_delete_schedule(schedule_id: int):
1419
+ try:
1420
+ delete_scheduled_crawl(schedule_id)
1421
+ return {'ok': True}
1422
+ except Exception as e:
1423
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1424
+
1425
+
1426
+ class GapRequest(BaseModel):
1427
+ your_keywords: list = []
1428
+ competitor_url: str
1429
+ max_pages: int = 3
1430
+
1431
+
1432
+ @app.post('/api/competitor/gap')
1433
+ async def api_competitor_gap(req: GapRequest):
1434
+ try:
1435
+ return {'ok': True, 'gap': competitor_keyword_gap(req.your_keywords, req.competitor_url, req.max_pages)}
1436
+ except Exception as e:
1437
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1438
+
1439
+
1440
+ class BulkRequest(BaseModel):
1441
+ urls: list
1442
+ org_name: str = ''
1443
+ max_pages: int = 2
1444
+
1445
+
1446
+ @app.post('/api/bulk/crawl')
1447
+ async def api_bulk_crawl(req: BulkRequest):
1448
+ try:
1449
+ job_ids = bulk_enqueue(req.urls, req.org_name, req.max_pages)
1450
+ return {'ok': True, 'job_ids': job_ids, 'count': len(job_ids)}
1451
+ except Exception as e:
1452
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1453
+
1454
+
1455
+ class EmailReportRequest(BaseModel):
1456
+ to_email: str
1457
+ url: str
1458
+
1459
+
1460
+ @app.post('/api/reports/email')
1461
+ async def api_send_email_report(req: EmailReportRequest):
1462
+ try:
1463
+ ok = send_weekly_report(req.to_email, req.url)
1464
+ if ok:
1465
+ return {'ok': True, 'message': f'تم الإرسال إلى {req.to_email}'}
1466
+ return JSONResponse({'ok': False, 'error': 'فشل الإرسال — أضف SMTP_HOST و SMTP_USER و SMTP_PASS في .env'}, status_code=400)
1467
+ except Exception as e:
1468
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1469
+
1470
+
1471
+ _SETTINGS_PATH = Path(os.environ.get('OUTPUT_DIR', Path(__file__).resolve().parent.parent / 'output')) / 'settings.json'
1472
+
1473
+
1474
+ def _load_settings() -> dict:
1475
+ if _SETTINGS_PATH.exists():
1476
+ try:
1477
+ return json.loads(_SETTINGS_PATH.read_text())
1478
+ except Exception:
1479
+ pass
1480
+ return {}
1481
+
1482
+
1483
+ def _save_settings(data: dict):
1484
+ _SETTINGS_PATH.write_text(json.dumps(data, ensure_ascii=False, indent=2))
1485
+
1486
+
1487
+ @app.get('/api/settings')
1488
+ async def api_get_settings():
1489
+ s = _load_settings()
1490
+ safe = {}
1491
+ for k, v in s.items():
1492
+ safe[k] = '***' if (v and (k.endswith('_key') or k.endswith('_KEY') or 'pass' in k.lower())) else v
1493
+ return {'ok': True, 'settings': safe}
1494
+
1495
+
1496
+ @app.post('/api/settings')
1497
+ async def api_save_settings(req: dict):
1498
+ try:
1499
+ current = _load_settings()
1500
+ for k, v in req.items():
1501
+ if v and v != '***':
1502
+ current[k] = v
1503
+ os.environ[k.upper()] = v
1504
+ _save_settings(current)
1505
+ return {'ok': True}
1506
+ except Exception as e:
1507
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1508
+
1509
+
1510
+ try:
1511
+ for k, v in _load_settings().items():
1512
+ if v and v != '***':
1513
+ os.environ.setdefault(k.upper(), v)
1514
+ except Exception:
1515
+ pass
1516
+
1517
+
1518
+ # ── Competitor Intelligence Analyzer ─────────────────────────────────────────
1519
+
1520
+ class CompetitorIntelRequest(BaseModel):
1521
+ url: str
1522
+ region: str = 'Saudi Arabia'
1523
+ industry: str = ''
1524
+ count: int = 7
1525
+ api_keys: dict = {}
1526
+
1527
+
1528
+ @app.post('/api/competitor/intelligence')
1529
+ async def api_competitor_intelligence(req: CompetitorIntelRequest):
1530
+ try:
1531
+ from server.competitor_intel import analyze_competitors
1532
+ result = analyze_competitors(
1533
+ req.url, region=req.region,
1534
+ industry=req.industry, count=req.count,
1535
+ api_keys=req.api_keys
1536
+ )
1537
+ return {'ok': True, 'result': result}
1538
+ except Exception as e:
1539
+ return JSONResponse({'ok': False, 'error': str(e)}, status_code=500)
1540
+
1541
+
1542
+ @app.get('/competitor-intel.html')
1543
+ @app.get('/competitor-intel')
1544
+ async def serve_competitor_intel():
1545
+ return FileResponse(str(frontend_dir / 'competitor-intel.html'))
server/competitor_intel.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Competitor Intelligence Analyzer
3
+ Uses: SerpAPI (find competitors) + Google PageSpeed API (free perf scores) + Groq (AI analysis)
4
+ """
5
+ import os
6
+ import re
7
+ import requests
8
+ from typing import List, Dict, Optional
9
+ from urllib.parse import urlparse
10
+
11
+
12
+ PAGESPEED_API = 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed'
13
+ SERPAPI_URL = 'https://serpapi.com/search'
14
+ ZENSERP_URL = 'https://app.zenserp.com/api/v2/search'
15
+
16
+ REGION_MAP = {
17
+ 'Saudi Arabia': {'gl': 'sa', 'hl': 'ar', 'location': 'Saudi Arabia', 'domain': 'google.com.sa'},
18
+ 'Egypt': {'gl': 'eg', 'hl': 'ar', 'location': 'Egypt', 'domain': 'google.com.eg'},
19
+ 'UAE': {'gl': 'ae', 'hl': 'ar', 'location': 'United Arab Emirates', 'domain': 'google.ae'},
20
+ 'Kuwait': {'gl': 'kw', 'hl': 'ar', 'location': 'Kuwait', 'domain': 'google.com.kw'},
21
+ 'Jordan': {'gl': 'jo', 'hl': 'ar', 'location': 'Jordan', 'domain': 'google.jo'},
22
+ 'Global': {'gl': 'us', 'hl': 'en', 'location': 'United States','domain': 'google.com'},
23
+ }
24
+
25
+
26
+ def _extract_domain(url: str) -> str:
27
+ try:
28
+ return urlparse(url).netloc.replace('www.', '')
29
+ except Exception:
30
+ return url
31
+
32
+
33
+ def _serp_search(query: str, region: str, api_key: str = None) -> List[Dict]:
34
+ """Search Google via SerpAPI or ZenSerp, return organic results."""
35
+ r = REGION_MAP.get(region, REGION_MAP['Global'])
36
+ key = api_key or os.getenv('SERPAPI_KEY', '')
37
+
38
+ if key:
39
+ try:
40
+ resp = requests.get(SERPAPI_URL, params={
41
+ 'q': query, 'location': r['location'],
42
+ 'hl': r['hl'], 'gl': r['gl'],
43
+ 'google_domain': r['domain'], 'api_key': key,
44
+ 'num': 10
45
+ }, timeout=15)
46
+ resp.raise_for_status()
47
+ data = resp.json()
48
+ return data.get('organic_results', [])
49
+ except Exception:
50
+ pass
51
+
52
+ # ZenSerp fallback
53
+ zen_key = os.getenv('ZENSERP_KEY', '')
54
+ if zen_key:
55
+ try:
56
+ resp = requests.get(ZENSERP_URL, params={
57
+ 'q': query, 'location': r['location'],
58
+ 'hl': r['hl'], 'gl': r['gl'], 'apikey': zen_key, 'num': 10
59
+ }, timeout=15)
60
+ resp.raise_for_status()
61
+ data = resp.json()
62
+ return data.get('organic', [])
63
+ except Exception:
64
+ pass
65
+
66
+ return []
67
+
68
+
69
+ def get_pagespeed(url: str) -> Dict:
70
+ """Get Google PageSpeed score — completely free, no key needed."""
71
+ try:
72
+ resp = requests.get(PAGESPEED_API, params={
73
+ 'url': url, 'strategy': 'mobile',
74
+ 'category': ['performance', 'seo', 'accessibility']
75
+ }, timeout=20)
76
+ resp.raise_for_status()
77
+ data = resp.json()
78
+ cats = data.get('lighthouseResult', {}).get('categories', {})
79
+ audits = data.get('lighthouseResult', {}).get('audits', {})
80
+ return {
81
+ 'performance': round((cats.get('performance', {}).get('score', 0) or 0) * 100),
82
+ 'seo': round((cats.get('seo', {}).get('score', 0) or 0) * 100),
83
+ 'accessibility': round((cats.get('accessibility', {}).get('score', 0) or 0) * 100),
84
+ 'fcp': audits.get('first-contentful-paint', {}).get('displayValue', '—'),
85
+ 'lcp': audits.get('largest-contentful-paint', {}).get('displayValue', '—'),
86
+ 'cls': audits.get('cumulative-layout-shift', {}).get('displayValue', '—'),
87
+ 'tbt': audits.get('total-blocking-time', {}).get('displayValue', '—'),
88
+ }
89
+ except Exception:
90
+ return {'performance': None, 'seo': None, 'accessibility': None,
91
+ 'fcp': '—', 'lcp': '—', 'cls': '—', 'tbt': '—'}
92
+
93
+
94
+ def _ai_analyze_competitors(your_domain: str, competitors: List[Dict],
95
+ industry: str, region: str, api_keys: dict = None) -> Dict:
96
+ """Use Groq/OpenAI to generate strategic competitor analysis."""
97
+ api_keys = api_keys or {}
98
+ groq_key = api_keys.get('groq') or os.getenv('GROQ_API_KEY', '')
99
+ openai_key = api_keys.get('openai') or os.getenv('OPENAI_API_KEY', '')
100
+
101
+ comp_summary = '\n'.join([
102
+ f"- {c['domain']}: perf={c.get('pagespeed', {}).get('performance', '?')}%, "
103
+ f"seo={c.get('pagespeed', {}).get('seo', '?')}%, "
104
+ f"snippet: {c.get('snippet', '')[:100]}"
105
+ for c in competitors[:6]
106
+ ])
107
+
108
+ prompt = f"""You are a competitive intelligence analyst for {region}.
109
+
110
+ Target site: {your_domain}
111
+ Industry: {industry or 'Digital Services'}
112
+ Region: {region}
113
+
114
+ Competitors found:
115
+ {comp_summary}
116
+
117
+ Analyze and return ONLY valid JSON:
118
+ {{
119
+ "market_position": "Leader/Challenger/Niche/Newcomer",
120
+ "key_differentiators": ["what makes each competitor stand out"],
121
+ "your_opportunities": ["3-5 specific gaps you can exploit"],
122
+ "threats": ["2-3 main competitive threats"],
123
+ "recommended_keywords": ["5 keywords competitors rank for that you should target"],
124
+ "quick_wins": ["3 immediate actions to outrank competitors"],
125
+ "market_summary": "2-sentence market overview"
126
+ }}"""
127
+
128
+ try:
129
+ if groq_key:
130
+ from groq import Groq
131
+ client = Groq(api_key=groq_key)
132
+ resp = client.chat.completions.create(
133
+ model='llama-3.3-70b-versatile',
134
+ messages=[{'role': 'user', 'content': prompt}],
135
+ temperature=0.2, max_tokens=1000
136
+ )
137
+ text = resp.choices[0].message.content
138
+ elif openai_key:
139
+ from openai import OpenAI
140
+ client = OpenAI(api_key=openai_key)
141
+ resp = client.chat.completions.create(
142
+ model='gpt-4o-mini',
143
+ messages=[{'role': 'user', 'content': prompt}],
144
+ temperature=0.2, max_tokens=1000
145
+ )
146
+ text = resp.choices[0].message.content
147
+ else:
148
+ return _demo_analysis(your_domain, competitors, industry, region)
149
+
150
+ import json, re
151
+ m = re.search(r'\{.*\}', text, re.DOTALL)
152
+ if m:
153
+ return json.loads(m.group(0))
154
+ except Exception:
155
+ pass
156
+
157
+ return _demo_analysis(your_domain, competitors, industry, region)
158
+
159
+
160
+ def _demo_analysis(your_domain: str, competitors: List[Dict], industry: str, region: str) -> Dict:
161
+ return {
162
+ 'market_position': 'Challenger',
163
+ 'key_differentiators': [f"{c['domain']} يتميز بـ {c.get('snippet','')[:60]}" for c in competitors[:3]],
164
+ 'your_opportunities': [
165
+ f'استهداف كلمات {industry or "الخدمة"} في {region} بمحتوى عربي متخصص',
166
+ 'بناء صفحات landing محلية لكل مدينة رئيسية',
167
+ 'إضافة Schema LocalBusiness لتحسين الظهور المحلي',
168
+ ],
169
+ 'threats': [
170
+ f'{competitors[0]["domain"] if competitors else "المنافس الأول"} يحتل المرتبة الأولى',
171
+ 'المنافسون يستخدمون محتوى أطول وأكثر تفصيلاً',
172
+ ],
173
+ 'recommended_keywords': [f'{industry or "خدمة"} في {region}', f'أفضل {industry or "شركة"} {region}'],
174
+ 'quick_wins': [
175
+ 'أضف Groq API للحصول على تحليل ذكاء اصطناعي حقيقي',
176
+ 'أنشئ صفحة مقارنة مع المنافسين',
177
+ 'حسّن سرعة الموقع (PageSpeed < 2s)',
178
+ ],
179
+ 'market_summary': f'[وضع تجريبي] أضف Groq API للحصول على تحليل حقيقي لسوق {industry or "الخدمات"} في {region}.'
180
+ }
181
+
182
+
183
+ def analyze_competitors(your_url: str, region: str = 'Saudi Arabia',
184
+ industry: str = '', count: int = 7,
185
+ api_keys: dict = None) -> Dict:
186
+ """
187
+ Full competitor intelligence pipeline:
188
+ 1. Extract domain + build search queries
189
+ 2. Find competitors via SerpAPI (free 100/mo)
190
+ 3. Get PageSpeed scores (completely free)
191
+ 4. AI strategic analysis via Groq
192
+ """
193
+ api_keys = api_keys or {}
194
+ your_domain = _extract_domain(your_url)
195
+ r = REGION_MAP.get(region, REGION_MAP['Global'])
196
+
197
+ # Build search queries to find competitors
198
+ queries = []
199
+ if industry:
200
+ queries.append(f'{industry} agency {region}')
201
+ queries.append(f'best {industry} company {r["location"]}')
202
+ queries.append(f'site similar to {your_domain}')
203
+ queries.append(f'{industry or "digital marketing"} {r["location"]}')
204
+
205
+ # Collect unique competitor domains
206
+ seen_domains = {your_domain}
207
+ raw_competitors = []
208
+
209
+ for query in queries[:3]:
210
+ results = _serp_search(query, region, api_keys.get('serpapi') or api_keys.get('serp'))
211
+ for res in results:
212
+ link = res.get('link') or res.get('url', '')
213
+ domain = _extract_domain(link)
214
+ if domain and domain not in seen_domains and len(raw_competitors) < count:
215
+ seen_domains.add(domain)
216
+ raw_competitors.append({
217
+ 'domain': domain,
218
+ 'url': link,
219
+ 'title': res.get('title', domain),
220
+ 'snippet': res.get('snippet', ''),
221
+ 'position': res.get('position', len(raw_competitors) + 1),
222
+ })
223
+
224
+ # If no SERP key, use AI to suggest competitors
225
+ if not raw_competitors:
226
+ raw_competitors = _suggest_competitors_ai(your_domain, industry, region, count, api_keys)
227
+
228
+ # Get PageSpeed for each competitor (free, parallel-ish)
229
+ competitors = []
230
+ for comp in raw_competitors[:count]:
231
+ ps = get_pagespeed(comp['url'] or f"https://{comp['domain']}")
232
+ competitors.append({**comp, 'pagespeed': ps})
233
+
234
+ # Get your own PageSpeed
235
+ your_pagespeed = get_pagespeed(your_url)
236
+
237
+ # AI strategic analysis
238
+ ai_analysis = _ai_analyze_competitors(your_domain, competitors, industry, region, api_keys)
239
+
240
+ return {
241
+ 'your_domain': your_domain,
242
+ 'your_url': your_url,
243
+ 'your_pagespeed': your_pagespeed,
244
+ 'region': region,
245
+ 'industry': industry,
246
+ 'competitors': competitors,
247
+ 'competitor_count': len(competitors),
248
+ 'ai_analysis': ai_analysis,
249
+ 'data_sources': {
250
+ 'serp': bool(os.getenv('SERPAPI_KEY') or api_keys.get('serpapi')),
251
+ 'pagespeed': True,
252
+ 'ai': bool(os.getenv('GROQ_API_KEY') or api_keys.get('groq') or
253
+ os.getenv('OPENAI_API_KEY') or api_keys.get('openai'))
254
+ }
255
+ }
256
+
257
+
258
+ def _suggest_competitors_ai(domain: str, industry: str, region: str,
259
+ count: int, api_keys: dict) -> List[Dict]:
260
+ """When no SERP key, use AI to suggest likely competitors."""
261
+ groq_key = api_keys.get('groq') or os.getenv('GROQ_API_KEY', '')
262
+ openai_key = api_keys.get('openai') or os.getenv('OPENAI_API_KEY', '')
263
+
264
+ prompt = (f"List {count} real competitor websites for a {industry or 'digital services'} "
265
+ f"company in {region} similar to {domain}. "
266
+ f"Return ONLY a JSON array of objects: "
267
+ f'[{{"domain":"example.com","title":"Company Name","snippet":"brief description"}}]')
268
+ try:
269
+ text = ''
270
+ if groq_key:
271
+ from groq import Groq
272
+ r = Groq(api_key=groq_key).chat.completions.create(
273
+ model='llama-3.3-70b-versatile',
274
+ messages=[{'role': 'user', 'content': prompt}],
275
+ temperature=0.3, max_tokens=600
276
+ )
277
+ text = r.choices[0].message.content
278
+ elif openai_key:
279
+ from openai import OpenAI
280
+ r = OpenAI(api_key=openai_key).chat.completions.create(
281
+ model='gpt-4o-mini',
282
+ messages=[{'role': 'user', 'content': prompt}],
283
+ temperature=0.3, max_tokens=600
284
+ )
285
+ text = r.choices[0].message.content
286
+
287
+ if text:
288
+ import json, re
289
+ m = re.search(r'\[.*\]', text, re.DOTALL)
290
+ if m:
291
+ items = json.loads(m.group(0))
292
+ return [{'domain': i.get('domain',''), 'url': f"https://{i.get('domain','')}",
293
+ 'title': i.get('title',''), 'snippet': i.get('snippet',''),
294
+ 'position': idx+1} for idx, i in enumerate(items[:count])]
295
+ except Exception:
296
+ pass
297
+ return []
server/reports.py CHANGED
@@ -1,43 +1,195 @@
 
1
  import json
2
  from pathlib import Path
 
3
 
4
 
5
  def build_html_report(job_dir: str) -> str:
6
- """Create a simple HTML report combining audit and analysis JSON files located in job_dir."""
7
  p = Path(job_dir)
8
- audit_path = p / 'audit.json'
9
- analysis_path = p / 'analysis.json'
10
  audit = {}
11
  analysis = {}
12
- if audit_path.exists():
13
- try:
14
- audit = json.loads(audit_path.read_text(encoding='utf-8'))
15
- except Exception:
16
- audit = {}
17
- if analysis_path.exists():
18
- try:
19
- analysis = json.loads(analysis_path.read_text(encoding='utf-8'))
20
- except Exception:
21
- analysis = {}
22
-
23
- title = (audit.get('pages', [{}])[0].get('title') if audit.get('pages') else 'GEO Report') or 'GEO Report'
24
- html = [f"<html><head><meta charset=\"utf-8\"><title>{title}</title></head><body style='font-family:Arial;color:#111;background:#fff;padding:18px'>"]
25
- html.append(f"<h1>{title}</h1>")
26
- html.append('<h2>Summary</h2>')
27
- geo = analysis.get('geo_score') if isinstance(analysis, dict) else None
28
- if geo:
29
- html.append(f"<p>GEO Score: <strong>{geo.get('score')}%</strong> — {geo.get('status')}</p>")
30
- html.append('<h2>Audit</h2>')
31
- html.append('<pre>'+json.dumps(audit, ensure_ascii=False, indent=2)+'</pre>')
32
- html.append('<h2>Analysis</h2>')
33
- html.append('<pre>'+json.dumps(analysis, ensure_ascii=False, indent=2)+'</pre>')
34
- html.append('</body></html>')
35
- return '\n'.join(html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
 
37
 
38
  def try_render_pdf(html: str, out_path: Path) -> bool:
39
  try:
40
- # optional dependency: weasyprint
41
  from weasyprint import HTML
42
  HTML(string=html).write_pdf(str(out_path))
43
  return True
 
1
+ """Professional GEO Report Generator — client-ready HTML + PDF."""
2
  import json
3
  from pathlib import Path
4
+ from datetime import datetime
5
 
6
 
7
  def build_html_report(job_dir: str) -> str:
 
8
  p = Path(job_dir)
 
 
9
  audit = {}
10
  analysis = {}
11
+ recs = {}
12
+
13
+ for fname, target in [('audit.json', 'audit'), ('analysis.json', 'analysis'), ('recommendations.json', 'recs')]:
14
+ path = p / fname
15
+ if path.exists():
16
+ try:
17
+ data = json.loads(path.read_text(encoding='utf-8'))
18
+ if target == 'audit': audit = data
19
+ elif target == 'analysis': analysis = data
20
+ elif target == 'recs': recs = data
21
+ except Exception:
22
+ pass
23
+
24
+ pages = audit.get('pages', [])
25
+ org = audit.get('org_name') or (pages[0].get('title') if pages else 'Website')
26
+ url = audit.get('url') or (pages[0].get('url') if pages else '')
27
+ geo = analysis.get('geo_score') or {}
28
+ score = geo.get('score', 0)
29
+ status = geo.get('status', 'N/A')
30
+ breakdown = geo.get('breakdown', {})
31
+ actions = recs.get('actions', []) if isinstance(recs, dict) else []
32
+ per_page = recs.get('per_page', []) if isinstance(recs, dict) else []
33
+ date_str = datetime.utcnow().strftime('%Y-%m-%d')
34
+
35
+ score_color = '#10b981' if score >= 75 else '#f59e0b' if score >= 40 else '#ef4444'
36
+
37
+ def bar(val, max_val=20, color='#00f2ff'):
38
+ pct = min(100, int((val / max_val) * 100))
39
+ return f'<div style="background:#e5e7eb;border-radius:4px;height:8px;width:100%"><div style="background:{color};height:8px;border-radius:4px;width:{pct}%"></div></div>'
40
+
41
+ # Issues summary
42
+ total_issues = sum(len(p.get('issues', [])) for p in per_page)
43
+ total_pages = len(pages)
44
+
45
+ # Per-page rows
46
+ page_rows = ''
47
+ for pg in per_page[:20]:
48
+ issues = pg.get('issues', [])
49
+ color = '#ef4444' if len(issues) > 1 else '#f59e0b' if issues else '#10b981'
50
+ icon = '🔴' if len(issues) > 1 else '🟡' if issues else '🟢'
51
+ page_rows += f'''
52
+ <tr>
53
+ <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;max-width:300px;word-break:break-all">
54
+ <div style="font-weight:600;font-size:13px">{pg.get("title") or pg.get("url","")}</div>
55
+ <div style="color:#6b7280;font-size:11px">{pg.get("url","")}</div>
56
+ </td>
57
+ <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;text-align:center">{icon}</td>
58
+ <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;font-size:12px;color:{color}">
59
+ {"، ".join(issues) if issues else "✅ لا مشاكل"}
60
+ </td>
61
+ </tr>'''
62
+
63
+ # Action items
64
+ action_rows = ''
65
+ for i, a in enumerate(actions[:10], 1):
66
+ text = a.get('text', a) if isinstance(a, dict) else a
67
+ priority = a.get('priority', 'MEDIUM') if isinstance(a, dict) else 'MEDIUM'
68
+ p_color = '#ef4444' if priority == 'HIGH' else '#f59e0b' if priority == 'MEDIUM' else '#10b981'
69
+ action_rows += f'''
70
+ <tr>
71
+ <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;text-align:center;font-weight:700;color:#6b7280">{i}</td>
72
+ <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6">
73
+ <span style="background:{p_color}20;color:{p_color};padding:2px 8px;border-radius:4px;font-size:11px;font-weight:700">{priority}</span>
74
+ </td>
75
+ <td style="padding:10px 12px;border-bottom:1px solid #f3f4f6;font-size:13px">{text}</td>
76
+ </tr>'''
77
+
78
+ html = f'''<!DOCTYPE html>
79
+ <html lang="ar" dir="rtl">
80
+ <head>
81
+ <meta charset="utf-8">
82
+ <title>تقرير GEO — {org}</title>
83
+ <style>
84
+ * {{ box-sizing: border-box; margin: 0; padding: 0; }}
85
+ body {{ font-family: 'Segoe UI', Arial, sans-serif; background: #f9fafb; color: #111827; direction: rtl; }}
86
+ .page {{ max-width: 900px; margin: 0 auto; padding: 40px 24px; }}
87
+ .header {{ background: linear-gradient(135deg, #0f172a 0%, #1e293b 100%); color: white; padding: 40px; border-radius: 16px; margin-bottom: 32px; }}
88
+ .header h1 {{ font-size: 28px; font-weight: 800; margin-bottom: 8px; }}
89
+ .header p {{ color: #94a3b8; font-size: 14px; }}
90
+ .score-circle {{ display: inline-flex; align-items: center; justify-content: center; width: 100px; height: 100px; border-radius: 50%; border: 6px solid {score_color}; font-size: 28px; font-weight: 900; color: {score_color}; float: left; margin-right: 24px; }}
91
+ .card {{ background: white; border-radius: 12px; padding: 24px; margin-bottom: 24px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }}
92
+ .card h2 {{ font-size: 18px; font-weight: 700; margin-bottom: 16px; color: #1e293b; border-bottom: 2px solid #f1f5f9; padding-bottom: 10px; }}
93
+ .metric {{ display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }}
94
+ .metric-label {{ font-size: 13px; color: #6b7280; }}
95
+ .metric-val {{ font-size: 13px; font-weight: 700; }}
96
+ table {{ width: 100%; border-collapse: collapse; }}
97
+ th {{ background: #f8fafc; padding: 10px 12px; text-align: right; font-size: 12px; color: #6b7280; text-transform: uppercase; letter-spacing: 0.5px; border-bottom: 2px solid #e5e7eb; }}
98
+ .badge {{ display: inline-block; padding: 3px 10px; border-radius: 20px; font-size: 11px; font-weight: 700; }}
99
+ .footer {{ text-align: center; color: #9ca3af; font-size: 12px; margin-top: 40px; padding-top: 20px; border-top: 1px solid #e5e7eb; }}
100
+ @media print {{ body {{ background: white; }} .page {{ padding: 20px; }} }}
101
+ </style>
102
+ </head>
103
+ <body>
104
+ <div class="page">
105
+
106
+ <!-- Header -->
107
+ <div class="header">
108
+ <div style="display:flex;align-items:center;gap:24px">
109
+ <div class="score-circle">{score}%</div>
110
+ <div>
111
+ <h1>تقرير GEO — {org}</h1>
112
+ <p>{url}</p>
113
+ <p style="margin-top:8px">تاريخ التقرير: {date_str} &nbsp;·&nbsp; الحالة: <span style="color:{score_color};font-weight:700">{status}</span></p>
114
+ <p style="margin-top:4px">الصفحات المحللة: {total_pages} &nbsp;·&nbsp; المشاكل المكتشفة: {total_issues}</p>
115
+ </div>
116
+ </div>
117
+ </div>
118
+
119
+ <!-- GEO Score Breakdown -->
120
+ <div class="card">
121
+ <h2>📊 تفصيل درجة GEO</h2>
122
+ <div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
123
+ {''.join(f"""
124
+ <div>
125
+ <div class="metric">
126
+ <span class="metric-label">{label}</span>
127
+ <span class="metric-val" style="color:{'#10b981' if val>=15 else '#f59e0b' if val>=8 else '#ef4444'}">{val}/20</span>
128
+ </div>
129
+ {bar(val)}
130
+ </div>""" for label, val in [
131
+ ('جودة العناوين', breakdown.get('headings', 0)),
132
+ ('كثافة المحتوى', breakdown.get('density', 0)),
133
+ ('الكيانات الدلالية', breakdown.get('entities', 0)),
134
+ ('أسئلة FAQ', breakdown.get('faq', 0)),
135
+ ('الظهور في الذكاء الاصطناعي', breakdown.get('ai_visibility', 0)),
136
+ ])}
137
+ </div>
138
+ </div>
139
+
140
+ <!-- Action Plan -->
141
+ <div class="card">
142
+ <h2>💡 خطة العمل ({len(actions)} توصية)</h2>
143
+ {'<p style="color:#6b7280;font-size:13px">لا توجد توصيات — شغّل تحليل الذكاء الاصطناعي أولاً.</p>' if not actions else f'''
144
+ <table>
145
+ <thead><tr><th>#</th><th>الأولوية</th><th>الإجراء المطلوب</th></tr></thead>
146
+ <tbody>{action_rows}</tbody>
147
+ </table>'''}
148
+ </div>
149
+
150
+ <!-- Per-page Analysis -->
151
+ <div class="card">
152
+ <h2>🔍 تحليل الصفحات ({total_pages} صفحة)</h2>
153
+ {'<p style="color:#6b7280;font-size:13px">لا توجد بيانات صفحات.</p>' if not per_page else f'''
154
+ <table>
155
+ <thead><tr><th>الصفحة</th><th>الحالة</th><th>المشاكل</th></tr></thead>
156
+ <tbody>{page_rows}</tbody>
157
+ </table>'''}
158
+ </div>
159
+
160
+ <!-- AI Visibility -->
161
+ {'<div class="card"><h2>🤖 الظهور في الذكاء الاصطناعي</h2>' + _render_ai_vis(audit.get('ai_visibility', {})) + '</div>' if audit.get('ai_visibility') else ''}
162
+
163
+ <div class="footer">
164
+ <p>تم إنشاء هذا التقرير بواسطة <strong>GEO Platform</strong> — منصة تحسين الظهور في محركات البحث الذكية</p>
165
+ <p style="margin-top:4px">{date_str}</p>
166
+ </div>
167
+
168
+ </div>
169
+ </body>
170
+ </html>'''
171
+ return html
172
+
173
+
174
+ def _render_ai_vis(ai_vis: dict) -> str:
175
+ if not ai_vis or not ai_vis.get('enabled'):
176
+ reason = ai_vis.get('reason', 'بيانات الظهور غير متاحة — أضف مفتاح Perplexity أو OpenAI') if ai_vis else 'غير مفعّل'
177
+ return f'<p style="color:#6b7280;font-size:13px">{reason}</p>'
178
+ results = ai_vis.get('results', [])
179
+ rows = ''.join(f'''
180
+ <tr>
181
+ <td style="padding:8px 12px;border-bottom:1px solid #f3f4f6;font-size:13px">{r.get("query","")}</td>
182
+ <td style="padding:8px 12px;border-bottom:1px solid #f3f4f6;text-align:center">
183
+ <span style="color:{'#10b981' if r.get('mentioned') else '#ef4444'};font-weight:700">
184
+ {'✅ موجود' if r.get('mentioned') else '❌ غائب'}
185
+ </span>
186
+ </td>
187
+ </tr>''' for r in results)
188
+ return f'<table><thead><tr><th>الاستعلام</th><th>النتيجة</th></tr></thead><tbody>{rows}</tbody></table>'
189
 
190
 
191
  def try_render_pdf(html: str, out_path: Path) -> bool:
192
  try:
 
193
  from weasyprint import HTML
194
  HTML(string=html).write_pdf(str(out_path))
195
  return True
server/worker.py CHANGED
@@ -66,6 +66,19 @@ def process_job(job):
66
  print(f"Failed to copy job results to main output dir: {copy_err}")
67
 
68
  job_queue.update_job(jid, status='completed', progress={'stage':'done','percent':100}, result_path=str(out_dir))
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  except Exception as e:
70
  job_queue.update_job(jid, status='failed', progress={'stage':'error','error': str(e)})
71
 
 
66
  print(f"Failed to copy job results to main output dir: {copy_err}")
67
 
68
  job_queue.update_job(jid, status='completed', progress={'stage':'done','percent':100}, result_path=str(out_dir))
69
+
70
+ # ── Save tracking snapshots & check alerts ────────────────────────
71
+ try:
72
+ from server.advanced_features import (
73
+ save_keyword_snapshot, save_geo_score_snapshot, check_and_create_alerts
74
+ )
75
+ from server.keyword_engine import extract_keywords_from_audit
76
+ kws = extract_keywords_from_audit({'pages': pages}, top_n=30, enrich=False)
77
+ save_keyword_snapshot(jid, job.get('url', ''), kws if isinstance(kws, list) else kws.get('top_keywords', []))
78
+ save_geo_score_snapshot(jid, job.get('url', ''), geo)
79
+ check_and_create_alerts(jid, job.get('url', ''), geo)
80
+ except Exception as track_err:
81
+ print(f'Tracking error (non-fatal): {track_err}')
82
  except Exception as e:
83
  job_queue.update_job(jid, status='failed', progress={'stage':'error','error': str(e)})
84