Kevinyogap commited on
Commit
d10e805
·
1 Parent(s): f643a4f

Add Flask SEO Meter app

Browse files
Files changed (3) hide show
  1. Dockerfile +12 -0
  2. app.py +347 -0
  3. requirements.txt +6 -0
Dockerfile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt requirements.txt
6
+ RUN pip install --upgrade pip && pip install -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ ENV FLASK_APP=app.py
11
+
12
+ CMD ["flask", "run", "--host=0.0.0.0", "--port=7860"]
app.py ADDED
@@ -0,0 +1,347 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ import re
3
+ import json
4
+ from typing import Dict, Union
5
+
6
+ app = Flask(__name__)
7
+
8
+ # ====================== COMMON UTILITY FUNCTIONS ======================
9
+ def check_keyword_exist(text: str, keyword: str) -> bool:
10
+ """Cek apakah keyword ada dalam teks"""
11
+ if not text or not keyword:
12
+ return False
13
+ keywords = [kw.strip().lower() for kw in keyword.split(',') if kw.strip()]
14
+ text_lower = text.lower()
15
+ return any(kw in text_lower for kw in keywords)
16
+
17
+ def format_score(score: Union[int, float]) -> Union[int, float]:
18
+ """Format skor untuk output"""
19
+ if isinstance(score, float):
20
+ return int(score) if score.is_integer() else round(score, 2)
21
+ return score
22
+
23
+ def calculate_keyword_density(text: str, keyword: str) -> float:
24
+ """Menghitung kepadatan keyword dalam teks"""
25
+ if not text or not keyword:
26
+ return 0.0
27
+ total_words = len(text.split())
28
+ keyword_count = sum(text.lower().count(kw.lower()) for kw in keyword.split(','))
29
+ return (keyword_count / max(1, total_words)) * 100
30
+
31
+ # ====================== TITLE ANALYSIS ======================
32
+ def calculate_title_score(article_data: Dict[str, Union[str, Dict]]) -> Dict[str, float]:
33
+ """Menghitung skor SEO untuk bagian Title Page"""
34
+ title = article_data.get('title', '')
35
+ target_keyword = article_data.get('target-keyword', '')
36
+
37
+ title_scores = {
38
+ 'keyword_exist_score': 0,
39
+ 'keyword_position_score': 0.0,
40
+ 'title_length_score': 0.0,
41
+ 'title_total_score': 0.0,
42
+ 'statuses': {
43
+ 'keyword_exist': '',
44
+ 'keyword_position': '',
45
+ 'title_length': '',
46
+ 'overall': ''
47
+ }
48
+ }
49
+
50
+ # 1. Target Keyword Exist (Bobot: 6%)
51
+ keyword_exist = check_keyword_exist(title, target_keyword)
52
+ title_scores['keyword_exist_score'] = 6 if keyword_exist else 0
53
+ title_scores['statuses']['keyword_exist'] = 'Good' if keyword_exist else 'Bad'
54
+
55
+ # 2. Target Keyword Position (Bobot: 3%)
56
+ if keyword_exist:
57
+ position_score = check_keyword_position(title, target_keyword)
58
+ title_scores['keyword_position_score'] = position_score * 3
59
+ if position_score == 1.0:
60
+ title_scores['statuses']['keyword_position'] = 'Good'
61
+ elif position_score == 0.5:
62
+ title_scores['statuses']['keyword_position'] = 'Needs Improvement'
63
+ else:
64
+ title_scores['statuses']['keyword_position'] = 'Bad'
65
+
66
+ # 3. Title Length (Bobot: 1%)
67
+ length_score = check_title_length(title)
68
+ title_scores['title_length_score'] = length_score * 1
69
+ if length_score == 1.0:
70
+ title_scores['statuses']['title_length'] = 'Good'
71
+ elif length_score == 0.5:
72
+ title_scores['statuses']['title_length'] = 'Needs Improvement'
73
+ else:
74
+ title_scores['statuses']['title_length'] = 'Bad'
75
+
76
+ # Hitung total skor title
77
+ title_scores['title_total_score'] = (
78
+ float(title_scores['keyword_exist_score']) +
79
+ title_scores['keyword_position_score'] +
80
+ title_scores['title_length_score']
81
+ )
82
+
83
+ # Determine overall status
84
+ total_percentage = (title_scores['title_total_score'] / 10) * 100
85
+ if total_percentage >= 80:
86
+ title_scores['statuses']['overall'] = 'Good'
87
+ elif total_percentage >= 50:
88
+ title_scores['statuses']['overall'] = 'Needs Improvement'
89
+ else:
90
+ title_scores['statuses']['overall'] = 'Bad'
91
+
92
+ return title_scores
93
+
94
+ def check_keyword_position(title: str, keyword: str) -> float:
95
+ """Cek posisi keyword dalam title"""
96
+ if not title or not keyword:
97
+ return 0.0
98
+ first_keyword = keyword.split(',')[0].strip().lower()
99
+ title_words = title.lower().split()
100
+ try:
101
+ keyword_index = title_words.index(first_keyword)
102
+ except ValueError:
103
+ return 0.0
104
+ words_before = keyword_index
105
+ if words_before <= 2: return 1.0
106
+ if words_before <= 4: return 0.5
107
+ return 0.0
108
+
109
+ def check_title_length(title: str) -> float:
110
+ """Cek panjang title"""
111
+ length = len(title)
112
+ if 75 <= length <= 95: return 1.0
113
+ if (40 <= length <= 74) or (95 < length <= 120): return 0.5
114
+ return 0.0
115
+
116
+ # ====================== META DESCRIPTION ANALYSIS ======================
117
+ def calculate_meta_desc_score(article_data: Dict[str, Union[str, Dict]]) -> Dict[str, float]:
118
+ """Menghitung skor SEO untuk bagian Meta Description"""
119
+ meta_desc = article_data.get('meta_desc', '')
120
+ target_keyword = article_data.get('target-keyword', '')
121
+ related_keyword = article_data.get('related-keyword', '')
122
+
123
+ meta_scores = {
124
+ 'keyword_exist_score': 0.0,
125
+ 'related_keyword_score': 0.0,
126
+ 'length_score': 0.0,
127
+ 'meta_total_score': 0.0,
128
+ 'statuses': {
129
+ 'keyword_exist': '',
130
+ 'related_keyword': '',
131
+ 'length': '',
132
+ 'overall': ''
133
+ }
134
+ }
135
+
136
+ # 1. Target Keyword Exist (Bobot: 1%)
137
+ keyword_exist = check_keyword_exist(meta_desc, target_keyword)
138
+ meta_scores['keyword_exist_score'] = 1.0 if keyword_exist else 0.0
139
+ meta_scores['statuses']['keyword_exist'] = 'Good' if keyword_exist else 'Bad'
140
+
141
+ # 2. Related Keyword Exist (Bobot: 3.5%)
142
+ related_exist = check_keyword_exist(meta_desc, related_keyword) if related_keyword else False
143
+ meta_scores['related_keyword_score'] = 3.5 if related_exist else 0.0
144
+ meta_scores['statuses']['related_keyword'] = 'Good' if related_exist else 'Bad'
145
+
146
+ # 3. Meta Description Length (Bobot: 0.5%)
147
+ length_status = check_meta_desc_length(meta_desc)
148
+ if length_status == 1.0:
149
+ meta_scores['length_score'] = 0.5
150
+ meta_scores['statuses']['length'] = 'Good'
151
+ elif length_status == 0.5:
152
+ meta_scores['length_score'] = 0.25
153
+ meta_scores['statuses']['length'] = 'Needs Improvement'
154
+ else:
155
+ meta_scores['length_score'] = 0.0
156
+ meta_scores['statuses']['length'] = 'Bad'
157
+
158
+ # Hitung total skor meta description
159
+ meta_scores['meta_total_score'] = (
160
+ meta_scores['keyword_exist_score'] +
161
+ meta_scores['related_keyword_score'] +
162
+ meta_scores['length_score']
163
+ )
164
+
165
+ # Determine overall status
166
+ total_percentage = (meta_scores['meta_total_score'] / 5) * 100
167
+ if total_percentage >= 80:
168
+ meta_scores['statuses']['overall'] = 'Good'
169
+ elif total_percentage >= 50:
170
+ meta_scores['statuses']['overall'] = 'Needs Improvement'
171
+ else:
172
+ meta_scores['statuses']['overall'] = 'Bad'
173
+
174
+ return meta_scores
175
+
176
+ def check_meta_desc_length(meta_desc: str) -> float:
177
+ """Cek panjang meta description"""
178
+ length = len(meta_desc)
179
+ if 126 <= length <= 146: return 1.0
180
+ if (100 <= length <= 125) or (146 < length <= 160): return 0.5
181
+ return 0.0
182
+
183
+ # ====================== CONTENT ANALYSIS ======================
184
+ def calculate_content_score(article_data: Dict[str, Union[str, Dict]]) -> Dict[str, float]:
185
+ """Menghitung skor SEO untuk bagian Konten"""
186
+ content = article_data.get('content', '')
187
+ target_keyword = article_data.get('target-keyword', '')
188
+ related_keyword = article_data.get('related-keyword', '')
189
+
190
+ content_scores = {
191
+ 'word_count_score': 0.0,
192
+ 'first_para_score': 0.0,
193
+ 'last_para_score': 0.0,
194
+ 'alt_image_score': 0.0,
195
+ 'keyword_density_score': 0.0,
196
+ 'related_keyword_density_score': 0.0,
197
+ 'keyword_frequency_score': 0.0,
198
+ 'content_total_score': 0.0,
199
+ 'statuses': {
200
+ 'word_count': '',
201
+ 'first_paragraph': '',
202
+ 'last_paragraph': '',
203
+ 'alt_image': '',
204
+ 'keyword_density': '',
205
+ 'related_keyword_density': '',
206
+ 'keyword_frequency': '',
207
+ 'overall': ''
208
+ }
209
+ }
210
+
211
+ # Clean HTML content
212
+ text_content = re.sub(r'<a href="#"[^>]*>.*?</a>', '', content)
213
+ text_content = re.sub('<[^<]+?>', '', text_content)
214
+ paragraphs = [p.strip() for p in text_content.split('\n') if p.strip()]
215
+
216
+ # 1. Word Count (14.5%)
217
+ word_count = len(text_content.split())
218
+ if word_count > 400:
219
+ content_scores['word_count_score'] = 14.5
220
+ content_scores['statuses']['word_count'] = 'Good'
221
+ elif word_count > 200:
222
+ content_scores['word_count_score'] = 7.25
223
+ content_scores['statuses']['word_count'] = 'Needs Improvement'
224
+ else:
225
+ content_scores['statuses']['word_count'] = 'Bad'
226
+
227
+ # 2. Target Keyword in First Paragraph (1.7%)
228
+ if paragraphs and check_keyword_exist(paragraphs[0], target_keyword):
229
+ content_scores['first_para_score'] = 1.7
230
+ content_scores['statuses']['first_paragraph'] = 'Good'
231
+
232
+ # 3. Target Keyword in Last Paragraph (1.7%)
233
+ if paragraphs and check_keyword_exist(paragraphs[-1], target_keyword):
234
+ content_scores['last_para_score'] = 1.7
235
+ content_scores['statuses']['last_paragraph'] = 'Good'
236
+
237
+ # 4. Target Keyword in Alt Image (0.9%)
238
+ alt_images = re.findall(r'alt=["\'](.*?)["\']', content)
239
+ if any(check_keyword_exist(alt, target_keyword) for alt in alt_images):
240
+ content_scores['alt_image_score'] = 0.9
241
+ content_scores['statuses']['alt_image'] = 'Good'
242
+
243
+ # 5. Keyword Density (14.9%)
244
+ keyword_density = calculate_keyword_density(text_content, target_keyword)
245
+ if 2.5 <= keyword_density <= 5:
246
+ content_scores['keyword_density_score'] = 14.9
247
+ content_scores['statuses']['keyword_density'] = 'Good'
248
+ elif keyword_density > 5:
249
+ content_scores['keyword_density_score'] = 7.45
250
+ content_scores['statuses']['keyword_density'] = 'Needs Improvement'
251
+
252
+ # 6. Related Keyword Density (14.9%)
253
+ if related_keyword:
254
+ related_density = calculate_keyword_density(text_content, related_keyword)
255
+ if 1 <= related_density <= 2:
256
+ content_scores['related_keyword_density_score'] = 14.9
257
+ content_scores['statuses']['related_keyword_density'] = 'Good'
258
+ elif related_density < 1 or (2 < related_density < 5):
259
+ content_scores['related_keyword_density_score'] = 7.45
260
+ content_scores['statuses']['related_keyword_density'] = 'Needs Improvement'
261
+
262
+ # 7. Keyword Frequency (25.5%)
263
+ keyword_count = sum(text_content.lower().count(kw.lower()) for kw in target_keyword.split(','))
264
+ if 3 <= keyword_count <= 6:
265
+ content_scores['keyword_frequency_score'] = 25.5
266
+ content_scores['statuses']['keyword_frequency'] = 'Good'
267
+ elif 1 <= keyword_count <= 2:
268
+ content_scores['keyword_frequency_score'] = 12.75
269
+ content_scores['statuses']['keyword_frequency'] = 'Needs Improvement'
270
+
271
+ # Calculate total score (85% maksimal tanpa internal link)
272
+ content_scores['content_total_score'] = sum([
273
+ content_scores['word_count_score'],
274
+ content_scores['first_para_score'],
275
+ content_scores['last_para_score'],
276
+ content_scores['alt_image_score'],
277
+ content_scores['keyword_density_score'],
278
+ content_scores['related_keyword_density_score'],
279
+ content_scores['keyword_frequency_score']
280
+ ])
281
+
282
+ # Determine overall status
283
+ total_percentage = (content_scores['content_total_score'] / 85) * 100
284
+ if total_percentage >= 80:
285
+ content_scores['statuses']['overall'] = 'Good'
286
+ elif total_percentage >= 50:
287
+ content_scores['statuses']['overall'] = 'Needs Improvement'
288
+ else:
289
+ content_scores['statuses']['overall'] = 'Bad'
290
+
291
+ return content_scores
292
+
293
+ # ====================== FLASK API ENDPOINTS ======================
294
+ @app.route('/analyze', methods=['POST'])
295
+ def analyze():
296
+ """Endpoint utama untuk analisis SEO"""
297
+ try:
298
+ article_data = request.get_json()
299
+
300
+ if not article_data:
301
+ return jsonify({"error": "No JSON data provided"}), 400
302
+
303
+ # Lakukan semua analisis
304
+ title_scores = calculate_title_score(article_data)
305
+ meta_scores = calculate_meta_desc_score(article_data)
306
+ content_scores = calculate_content_score(article_data)
307
+
308
+ # Hitung skor total
309
+ overall_score = (
310
+ title_scores['title_total_score'] +
311
+ meta_scores['meta_total_score'] +
312
+ content_scores['content_total_score']
313
+ )
314
+ max_score = 10 + 5 + 85 # Total maksimal semua komponen
315
+
316
+ # Siapkan response
317
+ response = {
318
+ "title_analysis": {
319
+ "scores": {k: format_score(v) for k, v in title_scores.items() if k.endswith('_score')},
320
+ "statuses": title_scores['statuses']
321
+ },
322
+ "meta_analysis": {
323
+ "scores": {k: format_score(v) for k, v in meta_scores.items() if k.endswith('_score')},
324
+ "statuses": meta_scores['statuses']
325
+ },
326
+ "content_analysis": {
327
+ "scores": {k: format_score(v) for k, v in content_scores.items() if k.endswith('_score')},
328
+ "statuses": content_scores['statuses']
329
+ },
330
+ "overall_score": {
331
+ "score": format_score(overall_score),
332
+ "max_score": max_score,
333
+ "percentage": round((overall_score / max_score) * 100, 2)
334
+ }
335
+ }
336
+
337
+ return jsonify(response)
338
+
339
+ except Exception as e:
340
+ return jsonify({"error": str(e)}), 500
341
+
342
+ @app.route('/')
343
+ def home():
344
+ return "SEO Analysis API - Send POST request to /analyze with article data"
345
+
346
+ if __name__ == '__main__':
347
+ app.run(debug=True)
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ flask==2.3.2
2
+ beautifulsoup4==4.12.2
3
+ lxml==4.9.3
4
+ requests==2.31.0
5
+ python-dotenv==1.0.0
6
+ gunicorn==20.1.0