Spaces:
Sleeping
Sleeping
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 +9 -0
- frontend/competitor-intel.html +424 -0
- frontend/index.html +6 -0
- frontend/recommendations.html +1 -0
- frontend/regional.html +1 -0
- frontend/search.html +1 -0
- output/analysis.json +7 -7
- output/audit.json +27 -804
- output/job-124/analysis.json +32 -23
- output/job-28/audit.json +6 -6
- output/job-87/audit.json +6 -6
- output/job-96/audit.json +6 -6
- output/job-97/audit.json +6 -6
- server/advanced_features.py +407 -0
- server/api.py +210 -0
- server/competitor_intel.py +297 -0
- server/reports.py +180 -28
- server/worker.py +13 -0
.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":
|
| 14 |
-
"status": "
|
| 15 |
"breakdown": {
|
| 16 |
-
"headings":
|
| 17 |
-
"density":
|
| 18 |
-
"entities":
|
| 19 |
-
"faq":
|
| 20 |
"ai_visibility": 0
|
| 21 |
},
|
| 22 |
"counts": {
|
| 23 |
-
"critical":
|
| 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://
|
| 5 |
-
"title": "
|
| 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 |
-
"
|
| 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://
|
| 288 |
-
"https://
|
| 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://
|
| 355 |
-
"title": "
|
| 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 |
-
"
|
| 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://
|
| 425 |
-
"https://
|
| 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://
|
| 466 |
-
"title": "
|
| 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":
|
| 712 |
-
"paras":
|
| 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://
|
| 733 |
-
"title": "
|
| 734 |
"headings_ok": false,
|
| 735 |
"density": {
|
| 736 |
-
"avg_words":
|
| 737 |
-
"paras":
|
| 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
|
| 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
|
| 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": "
|
| 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-
|
| 10 |
"parse_error": "No JSON found in LLM response"
|
| 11 |
}
|
| 12 |
},
|
|
@@ -26,72 +26,81 @@
|
|
| 26 |
"passed": 0
|
| 27 |
},
|
| 28 |
"v2": {
|
| 29 |
-
"score":
|
| 30 |
"breakdown": {
|
| 31 |
"seo_rank": 100.0,
|
| 32 |
"ai_visibility": 0.0,
|
| 33 |
-
"traffic":
|
| 34 |
},
|
| 35 |
"avg_rank": 1.0
|
| 36 |
}
|
| 37 |
},
|
| 38 |
"competitor_insight": {
|
| 39 |
-
"monthly_visits": "
|
| 40 |
"traffic_sources": {
|
| 41 |
-
"search":
|
| 42 |
"direct": 25,
|
| 43 |
-
"social":
|
| 44 |
-
"referral":
|
| 45 |
},
|
| 46 |
"top_competitors": [
|
| 47 |
{
|
| 48 |
"name": "Rabhan",
|
| 49 |
"domain": "rabhanagency.com",
|
| 50 |
-
"overlap_score":
|
| 51 |
"region": "SA"
|
| 52 |
},
|
| 53 |
{
|
| 54 |
"name": "Bayt.com",
|
| 55 |
"domain": "bayt.com",
|
| 56 |
-
"overlap_score":
|
| 57 |
"region": "Global"
|
| 58 |
},
|
| 59 |
{
|
| 60 |
-
"name": "
|
| 61 |
-
"domain": "
|
| 62 |
-
"overlap_score":
|
| 63 |
"region": "Global"
|
| 64 |
}
|
| 65 |
],
|
| 66 |
"regional_split": [
|
| 67 |
{
|
| 68 |
"country": "Saudi Arabia",
|
| 69 |
-
"share":
|
| 70 |
},
|
| 71 |
{
|
| 72 |
"country": "UAE",
|
| 73 |
-
"share":
|
| 74 |
},
|
| 75 |
{
|
| 76 |
"country": "Egypt",
|
| 77 |
"share": 10
|
| 78 |
},
|
| 79 |
{
|
| 80 |
-
"country": "
|
| 81 |
-
"share":
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
}
|
| 83 |
],
|
| 84 |
-
"industry": "
|
| 85 |
"seo_rankings": [
|
| 86 |
{
|
| 87 |
-
"query": "best
|
| 88 |
-
"rank":
|
| 89 |
-
"link": "https://rabhanagency.com/
|
| 90 |
},
|
| 91 |
{
|
| 92 |
-
"query": "top
|
| 93 |
-
"rank":
|
| 94 |
-
"link": "https://rabhanagency.com/
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 475 |
-
"https://mohrek.com/wp-content/uploads/
|
| 476 |
-
"https://mohrek.com/wp-content/uploads/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 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/
|
| 466 |
-
"https://mohrek.com/wp-content/uploads/
|
| 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/
|
| 476 |
"https://mohrek.com/?page_id=16015",
|
| 477 |
-
"https://mohrek.com/wp-content/uploads/
|
| 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/
|
| 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/
|
| 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/
|
| 466 |
-
"https://mohrek.com/wp-content/uploads/
|
| 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/
|
| 476 |
"https://mohrek.com/?page_id=16015",
|
| 477 |
-
"https://mohrek.com/wp-content/uploads/
|
| 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 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
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} · الحالة: <span style="color:{score_color};font-weight:700">{status}</span></p>
|
| 114 |
+
<p style="margin-top:4px">الصفحات المحللة: {total_pages} · المشاكل المكتشفة: {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 |
|