Spaces:
Sleeping
Sleeping
| from flask import Flask, request, jsonify | |
| import re | |
| import json | |
| from typing import Dict, Union | |
| app = Flask(__name__) | |
| # ====================== COMMON UTILITY FUNCTIONS ====================== | |
| def check_keyword_exist(text: str, keyword: str) -> bool: | |
| """Cek apakah keyword ada dalam teks""" | |
| if not text or not keyword: | |
| return False | |
| keywords = [kw.strip().lower() for kw in keyword.split(',') if kw.strip()] | |
| text_lower = text.lower() | |
| return any(kw in text_lower for kw in keywords) | |
| def format_score(score: Union[int, float]) -> Union[int, float]: | |
| """Format skor untuk output""" | |
| if isinstance(score, float): | |
| return int(score) if score.is_integer() else round(score, 2) | |
| return score | |
| def calculate_keyword_density(text: str, keyword: str) -> float: | |
| """Menghitung kepadatan keyword dalam teks""" | |
| if not text or not keyword: | |
| return 0.0 | |
| total_words = len(text.split()) | |
| keyword_count = sum(text.lower().count(kw.lower()) for kw in keyword.split(',')) | |
| return (keyword_count / max(1, total_words)) * 100 | |
| # ====================== TITLE ANALYSIS ====================== | |
| def calculate_title_score(article_data: Dict[str, Union[str, Dict]]) -> Dict[str, float]: | |
| """Menghitung skor SEO untuk bagian Title Page""" | |
| title = article_data.get('title', '') | |
| target_keyword = article_data.get('target-keyword', '') | |
| title_scores = { | |
| 'keyword_exist_score': 0, | |
| 'keyword_position_score': 0.0, | |
| 'title_length_score': 0.0, | |
| 'title_total_score': 0.0, | |
| 'statuses': { | |
| 'keyword_exist': '', | |
| 'keyword_position': '', | |
| 'title_length': '', | |
| 'overall': '' | |
| } | |
| } | |
| # 1. Target Keyword Exist (Bobot: 6%) | |
| keyword_exist = check_keyword_exist(title, target_keyword) | |
| title_scores['keyword_exist_score'] = 6 if keyword_exist else 0 | |
| title_scores['statuses']['keyword_exist'] = 'Good' if keyword_exist else 'Bad' | |
| # 2. Target Keyword Position (Bobot: 3%) | |
| if keyword_exist: | |
| position_score = check_keyword_position(title, target_keyword) | |
| title_scores['keyword_position_score'] = position_score * 3 | |
| if position_score == 1.0: | |
| title_scores['statuses']['keyword_position'] = 'Good' | |
| elif position_score == 0.5: | |
| title_scores['statuses']['keyword_position'] = 'Needs Improvement' | |
| else: | |
| title_scores['statuses']['keyword_position'] = 'Bad' | |
| # 3. Title Length (Bobot: 1%) | |
| length_score = check_title_length(title) | |
| title_scores['title_length_score'] = length_score * 1 | |
| if length_score == 1.0: | |
| title_scores['statuses']['title_length'] = 'Good' | |
| elif length_score == 0.5: | |
| title_scores['statuses']['title_length'] = 'Needs Improvement' | |
| else: | |
| title_scores['statuses']['title_length'] = 'Bad' | |
| # Hitung total skor title | |
| title_scores['title_total_score'] = ( | |
| float(title_scores['keyword_exist_score']) + | |
| title_scores['keyword_position_score'] + | |
| title_scores['title_length_score'] | |
| ) | |
| # Determine overall status | |
| total_percentage = (title_scores['title_total_score'] / 10) * 100 | |
| if total_percentage >= 80: | |
| title_scores['statuses']['overall'] = 'Good' | |
| elif total_percentage >= 50: | |
| title_scores['statuses']['overall'] = 'Needs Improvement' | |
| else: | |
| title_scores['statuses']['overall'] = 'Bad' | |
| return title_scores | |
| def check_keyword_position(title: str, keyword: str) -> float: | |
| """Cek posisi keyword dalam title""" | |
| if not title or not keyword: | |
| return 0.0 | |
| first_keyword = keyword.split(',')[0].strip().lower() | |
| title_words = title.lower().split() | |
| try: | |
| keyword_index = title_words.index(first_keyword) | |
| except ValueError: | |
| return 0.0 | |
| words_before = keyword_index | |
| if words_before <= 2: return 1.0 | |
| if words_before <= 4: return 0.5 | |
| return 0.0 | |
| def check_title_length(title: str) -> float: | |
| """Cek panjang title""" | |
| length = len(title) | |
| if 75 <= length <= 95: return 1.0 | |
| if (40 <= length <= 74) or (95 < length <= 120): return 0.5 | |
| return 0.0 | |
| # ====================== META DESCRIPTION ANALYSIS ====================== | |
| def calculate_meta_desc_score(article_data: Dict[str, Union[str, Dict]]) -> Dict[str, float]: | |
| """Menghitung skor SEO untuk bagian Meta Description""" | |
| meta_desc = article_data.get('meta_desc', '') | |
| target_keyword = article_data.get('target-keyword', '') | |
| related_keyword = article_data.get('related-keyword', '') | |
| meta_scores = { | |
| 'keyword_exist_score': 0.0, | |
| 'related_keyword_score': 0.0, | |
| 'length_score': 0.0, | |
| 'meta_total_score': 0.0, | |
| 'statuses': { | |
| 'keyword_exist': '', | |
| 'related_keyword': '', | |
| 'length': '', | |
| 'overall': '' | |
| } | |
| } | |
| # 1. Target Keyword Exist (Bobot: 1%) | |
| keyword_exist = check_keyword_exist(meta_desc, target_keyword) | |
| meta_scores['keyword_exist_score'] = 1.0 if keyword_exist else 0.0 | |
| meta_scores['statuses']['keyword_exist'] = 'Good' if keyword_exist else 'Bad' | |
| # 2. Related Keyword Exist (Bobot: 3.5%) | |
| related_exist = check_keyword_exist(meta_desc, related_keyword) if related_keyword else False | |
| meta_scores['related_keyword_score'] = 3.5 if related_exist else 0.0 | |
| meta_scores['statuses']['related_keyword'] = 'Good' if related_exist else 'Bad' | |
| # 3. Meta Description Length (Bobot: 0.5%) | |
| length_status = check_meta_desc_length(meta_desc) | |
| if length_status == 1.0: | |
| meta_scores['length_score'] = 0.5 | |
| meta_scores['statuses']['length'] = 'Good' | |
| elif length_status == 0.5: | |
| meta_scores['length_score'] = 0.25 | |
| meta_scores['statuses']['length'] = 'Needs Improvement' | |
| else: | |
| meta_scores['length_score'] = 0.0 | |
| meta_scores['statuses']['length'] = 'Bad' | |
| # Hitung total skor meta description | |
| meta_scores['meta_total_score'] = ( | |
| meta_scores['keyword_exist_score'] + | |
| meta_scores['related_keyword_score'] + | |
| meta_scores['length_score'] | |
| ) | |
| # Determine overall status | |
| total_percentage = (meta_scores['meta_total_score'] / 5) * 100 | |
| if total_percentage >= 80: | |
| meta_scores['statuses']['overall'] = 'Good' | |
| elif total_percentage >= 50: | |
| meta_scores['statuses']['overall'] = 'Needs Improvement' | |
| else: | |
| meta_scores['statuses']['overall'] = 'Bad' | |
| return meta_scores | |
| def check_meta_desc_length(meta_desc: str) -> float: | |
| """Cek panjang meta description""" | |
| length = len(meta_desc) | |
| if 126 <= length <= 146: return 1.0 | |
| if (100 <= length <= 125) or (146 < length <= 160): return 0.5 | |
| return 0.0 | |
| # ====================== CONTENT ANALYSIS ====================== | |
| def calculate_content_score(article_data: Dict[str, Union[str, Dict]]) -> Dict[str, float]: | |
| """Menghitung skor SEO untuk bagian Konten""" | |
| content = article_data.get('content', '') | |
| target_keyword = article_data.get('target-keyword', '') | |
| related_keyword = article_data.get('related-keyword', '') | |
| content_scores = { | |
| 'word_count_score': 0.0, | |
| 'first_para_score': 0.0, | |
| 'last_para_score': 0.0, | |
| 'alt_image_score': 0.0, | |
| 'keyword_density_score': 0.0, | |
| 'related_keyword_density_score': 0.0, | |
| 'keyword_frequency_score': 0.0, | |
| 'content_total_score': 0.0, | |
| 'statuses': { | |
| 'word_count': '', | |
| 'first_paragraph': '', | |
| 'last_paragraph': '', | |
| 'alt_image': '', | |
| 'keyword_density': '', | |
| 'related_keyword_density': '', | |
| 'keyword_frequency': '', | |
| 'overall': '' | |
| } | |
| } | |
| # Clean HTML content | |
| text_content = re.sub(r'<a href="#"[^>]*>.*?</a>', '', content) | |
| text_content = re.sub('<[^<]+?>', '', text_content) | |
| paragraphs = [p.strip() for p in text_content.split('\n') if p.strip()] | |
| # 1. Word Count (14.5%) | |
| word_count = len(text_content.split()) | |
| if word_count > 400: | |
| content_scores['word_count_score'] = 14.5 | |
| content_scores['statuses']['word_count'] = 'Good' | |
| elif word_count > 200: | |
| content_scores['word_count_score'] = 7.25 | |
| content_scores['statuses']['word_count'] = 'Needs Improvement' | |
| else: | |
| content_scores['statuses']['word_count'] = 'Bad' | |
| # 2. Target Keyword in First Paragraph (1.7%) | |
| if paragraphs and check_keyword_exist(paragraphs[0], target_keyword): | |
| content_scores['first_para_score'] = 1.7 | |
| content_scores['statuses']['first_paragraph'] = 'Good' | |
| # 3. Target Keyword in Last Paragraph (1.7%) | |
| if paragraphs and check_keyword_exist(paragraphs[-1], target_keyword): | |
| content_scores['last_para_score'] = 1.7 | |
| content_scores['statuses']['last_paragraph'] = 'Good' | |
| # 4. Target Keyword in Alt Image (0.9%) | |
| alt_images = re.findall(r'alt=["\'](.*?)["\']', content) | |
| if any(check_keyword_exist(alt, target_keyword) for alt in alt_images): | |
| content_scores['alt_image_score'] = 0.9 | |
| content_scores['statuses']['alt_image'] = 'Good' | |
| # 5. Keyword Density (14.9%) | |
| keyword_density = calculate_keyword_density(text_content, target_keyword) | |
| if 2.5 <= keyword_density <= 5: | |
| content_scores['keyword_density_score'] = 14.9 | |
| content_scores['statuses']['keyword_density'] = 'Good' | |
| elif keyword_density > 5: | |
| content_scores['keyword_density_score'] = 7.45 | |
| content_scores['statuses']['keyword_density'] = 'Needs Improvement' | |
| # 6. Related Keyword Density (14.9%) | |
| if related_keyword: | |
| related_density = calculate_keyword_density(text_content, related_keyword) | |
| if 1 <= related_density <= 2: | |
| content_scores['related_keyword_density_score'] = 14.9 | |
| content_scores['statuses']['related_keyword_density'] = 'Good' | |
| elif related_density < 1 or (2 < related_density < 5): | |
| content_scores['related_keyword_density_score'] = 7.45 | |
| content_scores['statuses']['related_keyword_density'] = 'Needs Improvement' | |
| # 7. Keyword Frequency (25.5%) | |
| keyword_count = sum(text_content.lower().count(kw.lower()) for kw in target_keyword.split(',')) | |
| if 3 <= keyword_count <= 6: | |
| content_scores['keyword_frequency_score'] = 25.5 | |
| content_scores['statuses']['keyword_frequency'] = 'Good' | |
| elif 1 <= keyword_count <= 2: | |
| content_scores['keyword_frequency_score'] = 12.75 | |
| content_scores['statuses']['keyword_frequency'] = 'Needs Improvement' | |
| # Calculate total score (85% maksimal tanpa internal link) | |
| content_scores['content_total_score'] = sum([ | |
| content_scores['word_count_score'], | |
| content_scores['first_para_score'], | |
| content_scores['last_para_score'], | |
| content_scores['alt_image_score'], | |
| content_scores['keyword_density_score'], | |
| content_scores['related_keyword_density_score'], | |
| content_scores['keyword_frequency_score'] | |
| ]) | |
| # Determine overall status | |
| total_percentage = (content_scores['content_total_score'] / 85) * 100 | |
| if total_percentage >= 80: | |
| content_scores['statuses']['overall'] = 'Good' | |
| elif total_percentage >= 50: | |
| content_scores['statuses']['overall'] = 'Needs Improvement' | |
| else: | |
| content_scores['statuses']['overall'] = 'Bad' | |
| return content_scores | |
| # ====================== FLASK API ENDPOINTS ====================== | |
| def analyze(): | |
| """Endpoint utama untuk analisis SEO""" | |
| try: | |
| article_data = request.get_json() | |
| if not article_data: | |
| return jsonify({"error": "No JSON data provided"}), 400 | |
| # Lakukan semua analisis | |
| title_scores = calculate_title_score(article_data) | |
| meta_scores = calculate_meta_desc_score(article_data) | |
| content_scores = calculate_content_score(article_data) | |
| # Hitung skor total | |
| overall_score = ( | |
| title_scores['title_total_score'] + | |
| meta_scores['meta_total_score'] + | |
| content_scores['content_total_score'] | |
| ) | |
| max_score = 10 + 5 + 85 # Total maksimal semua komponen | |
| # Siapkan response | |
| response = { | |
| "title_analysis": { | |
| "scores": {k: format_score(v) for k, v in title_scores.items() if k.endswith('_score')}, | |
| "statuses": title_scores['statuses'] | |
| }, | |
| "meta_analysis": { | |
| "scores": {k: format_score(v) for k, v in meta_scores.items() if k.endswith('_score')}, | |
| "statuses": meta_scores['statuses'] | |
| }, | |
| "content_analysis": { | |
| "scores": {k: format_score(v) for k, v in content_scores.items() if k.endswith('_score')}, | |
| "statuses": content_scores['statuses'] | |
| }, | |
| "overall_score": { | |
| "score": format_score(overall_score), | |
| "max_score": max_score, | |
| "percentage": round((overall_score / max_score) * 100, 2) | |
| } | |
| } | |
| return jsonify(response) | |
| except Exception as e: | |
| return jsonify({"error": str(e)}), 500 | |
| def home(): | |
| return "SEO Analysis API - Send POST request to /analyze with article data" | |
| if __name__ == '__main__': | |
| app.run(debug=True) |