Paste-a-URL site audit tools are a popular product: type in a web address, get a score and a list of fixes written for a non-technical owner. The hard part is the audit engine. If you rent that, the rest is a small web app. This series builds one in three parts:
- A report page from a URL (this post)
- PDF export with your own branding
- A free preview with a paid unlock
What you need
- Python 3.10+ with
flaskandrequests - An API key from seoscoreapi.com. The free tier's two audits a day is enough to build with.
The whole app
import os
from html import escape
import requests
from flask import Flask, request
API = "https://seoscoreapi.com/audit"
KEY = os.environ["SEOSCORE_API_KEY"]
app = Flask(__name__)
FORM = """<form><input name="url" placeholder="https://example.com" size="40">
<button>Audit</button></form>"""
OWNERS = {"developer": "Your developer", "content": "Whoever writes the site", "marketing": "Marketing"}
def run_audit(url: str) -> dict:
r = requests.get(API, params={"url": url}, headers={"X-API-Key": KEY}, timeout=60)
r.raise_for_status()
return r.json()
@app.get("/")
def home():
url = request.args.get("url", "").strip()
if not url:
return FORM
data = run_audit(url)
rows = "".join(
f"<li><strong>{escape(p['plain_english'])}</strong><br>"
f"<small>{escape(OWNERS.get(p['who_fixes_it'], ''))} · {escape(p['severity'])}"
f" · {escape(p['fix'])}</small></li>"
for p in data.get("priorities", [])
)
return (f"{FORM}<h1>{escape(data['url'])}: {round(data['score'])}/100 ({escape(data['grade'])})</h1>"
f"<h2>What to fix</h2><ol>{rows or '<li>Nothing urgent.</li>'}</ol>")
if __name__ == "__main__":
app.run(debug=True)
Run it with SEOSCORE_API_KEY=... python app.py and open http://127.0.0.1:5000.
What is doing the work
The response from /audit has an overall score and grade, and a priorities list sorted by importance. Each priority carries four fields this app uses:
| Field | Example |
|---|---|
plain_english |
"The page has no summary for search results, so Google picks a random snippet that may not make people click." |
who_fixes_it |
content |
severity |
high |
fix |
"Add a meta description of 120-160 characters" |
The app leads with the plain sentence and puts the technical fix in small type underneath. That ordering is the whole product idea: the owner reads the first line, and whoever does the work reads the second.
Two things to add before real users see it
Validate the input. The API refuses private and malformed addresses for you, but your own app should still reject empty input and show a friendly message when the API returns a 422 because the site couldn't be loaded:
r = requests.get(API, params={"url": url}, headers={"X-API-Key": KEY}, timeout=60)
if r.status_code == 422:
return {"error": r.json().get("detail", "That site could not be loaded.")}
Cache results. An audit takes a couple of seconds and counts against your quota. Store each result by URL for a day so a reload doesn't spend another one.
Next
The report is on screen. In part 2 it becomes a PDF with your logo on it.