seo / app.py
Hasnain-Mukhtiar7's picture
Create app.py
822c306 verified
import gradio as gr
import requests
import json
import pandas as pd
import numpy as np
import re
from bs4 import BeautifulSoup
from datetime import datetime
# ------------------ Utilities ------------------
def fetch_html(url, timeout=15):
try:
headers = {"User-Agent": "Mozilla/5.0 (compatible; LocalSEO/1.0; +https://example.com)"}
r = requests.get(url, headers=headers, timeout=timeout)
if r.status_code == 200:
return r.text
except Exception:
return ""
return ""
def extract_tag(soup, selector):
el = soup.select_one(selector)
return el.get_text(strip=True) if el else ""
def has_viewport(soup):
return soup.find("meta", attrs={"name": "viewport"}) is not None
def detect_schema_jsonld(soup):
schemas = []
for tag in soup.find_all("script", type="application/ld+json"):
try:
data = json.loads(tag.string or "{}")
schemas.append(data)
except Exception:
continue
return schemas
def find_phone_and_address(text):
phone_match = re.search(r'(\+?\d[\d\-\s\(\)]{7,}\d)', text)
address_match = re.search(r'(\d{1,4}\s+[A-Za-z0-9\.\-\,\s]+(?:Street|St|Road|Rd|Avenue|Ave|Boulevard|Blvd|Lane|Ln|Town|City|Area|Block|Sector|Phase|Colony).{0,40})',
text, flags=re.IGNORECASE)
phone = phone_match.group(1).strip() if phone_match else ""
address = address_match.group(1).strip() if address_match else ""
return phone, address
def grade_to_badge(score):
if score >= 90: return ("A+", "πŸŽ‰")
if score >= 80: return ("A", "βœ…")
if score >= 70: return ("B", "πŸ‘")
if score >= 60: return ("C", "🟠")
if score >= 50: return ("D", "⚠️")
return ("F", "❌")
def geocode_nominatim(place):
try:
r = requests.get("https://nominatim.openstreetmap.org/search",
params={"q": place, "format": "json", "limit": 1},
headers={"User-Agent": "LocalSEO-Toolkit/1.0"},
timeout=20)
if r.status_code == 200 and r.json():
j = r.json()[0]
return float(j["lat"]), float(j["lon"]), j.get("display_name", "")
except Exception:
pass
return None, None, ""
def call_pagespeed(url, api_key):
endpoint = "https://www.googleapis.com/pagespeedonline/v5/runPagespeed"
try:
r = requests.get(endpoint, params={"url": url, "key": api_key, "strategy": "mobile"}, timeout=30)
if r.status_code == 200:
return r.json()
else:
return {"error": r.text}
except Exception as e:
return {"error": str(e)}
def call_serper_local(q, api_key, gl="pk", hl="en"):
url = "https://google.serper.dev/search"
headers = {"X-API-KEY": api_key, "Content-Type": "application/json"}
payload = {"q": q, "gl": gl, "hl": hl, "autocorrect": True}
try:
resp = requests.post(url, headers=headers, data=json.dumps(payload), timeout=30)
if resp.status_code == 200:
return resp.json()
return {"error": resp.text}
except Exception as e:
return {"error": str(e)}
def sentiment_scores(texts):
try:
from nltk.sentiment import SentimentIntensityAnalyzer
import nltk
try:
nltk.data.find('sentiment/vader_lexicon.zip')
except LookupError:
nltk.download('vader_lexicon')
sia = SentimentIntensityAnalyzer()
scores = [sia.polarity_scores(t)["compound"] for t in texts]
return scores
except Exception:
return [0.0 for _ in texts]
# ------------------ Main Analyzer ------------------
def analyze(
business_name, primary_domain, target_city, category, nap_phone, nap_address, serper_key, psi_key
):
html = fetch_html(primary_domain)
if not html:
return f"❌ Could not fetch {primary_domain}"
soup = BeautifulSoup(html, "html.parser")
title = extract_tag(soup, "title")
meta_desc = extract_tag(soup, "meta[name='description']")
viewport = "Yes βœ…" if has_viewport(soup) else "No ❌"
phone, address = find_phone_and_address(html)
# PageSpeed
psi_score = None
if psi_key:
psi = call_pagespeed(primary_domain, psi_key)
psi_score = psi.get("lighthouseResult", {}).get("categories", {}).get("performance", {}).get("score")
if psi_score is not None:
psi_score = int(psi_score * 100)
# Serper local
serper_results = None
if serper_key:
serper_results = call_serper_local(business_name + " " + target_city, serper_key)
# Report assembly
report_md = f"""
# 🩺 Doctor of Local SEO Report
**Business:** {business_name}
**Website:** {primary_domain}
**Target City:** {target_city}
**Category:** {category}
---
### On-page Info
- Title: {title}
- Meta Description: {meta_desc}
- Viewport tag: {viewport}
### NAP (detected vs provided)
- Provided Phone: {nap_phone}
- Detected Phone: {phone or "Not found"}
- Provided Address: {nap_address}
- Detected Address: {address or "Not found"}
"""
if psi_score is not None:
grade, emoji = grade_to_badge(psi_score)
report_md += f"\n\n### PageSpeed Insights\n- Score: {psi_score} ({grade} {emoji})"
if serper_results and "organic" in serper_results:
top_results = [r.get("title", "") for r in serper_results["organic"][:3]]
report_md += f"\n\n### Top Local SERP Results\n" + "\n".join([f"- {t}" for t in top_results])
report_md += f"\n\n_(Generated {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')})_"
return report_md
# ------------------ Gradio Interface ------------------
iface = gr.Interface(
fn=analyze,
inputs=[
gr.Textbox(label="Business Name", value="Sample Dental Clinic"),
gr.Textbox(label="Website URL", value="https://example.com"),
gr.Textbox(label="Target City/Area", value="Karachi, Pakistan"),
gr.Dropdown(label="Category (OSM)", choices=["shop", "amenity", "office", "tourism", "healthcare"], value="shop"),
gr.Textbox(label="Your Phone (for NAP check)", value="+92 300 0000000"),
gr.Textbox(label="Your Address (for NAP check)", value="123 Main Street, Karachi"),
gr.Textbox(label="Serper.dev API Key (optional)", type="password"),
gr.Textbox(label="Google PSI API Key (optional)", type="password"),
],
outputs=gr.Markdown(label="SEO Audit Report"),
title="Doctor of Local SEO β€” Gradio Edition",
description="All-in-one Local SEO toolkit for quick audits. Supports PSI & Serper integrations.",
)
if __name__ == "__main__":
iface.launch()