A lightweight Python SEO tool inspired by OpenSEO
Welcome to another official UVF IT guide where we roll up our sleeves and build a tiny SEO analyzer, echoing the spirit of OpenSEO’s open‑source Semrush alternative. In just ~45 minutes you’ll have a local Flask app that fetches page titles, meta descriptions, and word counts without any paid APIs.
We’ll use Python, Flask, and SQLite – all free tier – to mimic the core insight features of OpenSEO. The result is a runnable prototype you can extend into a full‑blown visibility dashboard.
git installed; older versions may break the virtual‑env setup.python -m venv venv instead of python3 -m venv venv.First we create an isolated Python environment so our dependencies don’t clash with other projects. Then we install Flask for the web server and BeautifulSoup for HTML parsing. Finally we lay out a tiny folder tree that keeps routes, models, and static files tidy.
Having a clear scaffold makes later steps painless: you’ll know exactly where to drop new code and where the app entry point lives.
# create project folder
mkdir mini_seo && cd mini_seo
# set up virtual environment
python3 -m venv venv
source venv/bin/activate # on Windows use `venv\\Scripts\\activate`
# install core libraries
pip install Flask beautifulsoup4 requests
# basic folder layout
mkdir -p app/templates app/static app/models
# create minimal Flask init
cat > app/__init__.py <<'EOF'
from flask import Flask
app = Flask(__name__)
from . import routes
EOF
# placeholder route file
cat > app/routes.py <<'EOF'
from . import app
from flask import render_template, request, jsonify
@app.route('/')
def index():
return render_template('index.html')
EOF
# simple run script
cat > run.py <<'EOF'
from app import app
if __name__ == '__main__':
app.run(debug=True)
EOFWe need a persistent store for each URL we analyze. A single SQLite table is enough for a minimal clone; it holds the URL, the page title, the meta description, and a simple word‑count metric. Using SQLAlchemy keeps the code database‑agnostic and easy to query later.
The model lives in app/models.py and will be imported by the route handlers when we save analysis results.
app/models.py
from flask_sqlalchemy import SQLAlchemy
from app import app
# configure SQLite database (file will be created automatically)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///seo_results.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
class PageAnalysis(db.Model):
__tablename__ = 'page_analysis'
id = db.Column(db.Integer, primary_key=True)
url = db.Column(db.String(2083), unique=True, nullable=False)
title = db.Column(db.String(255))
description = db.Column(db.Text)
word_count = db.Column(db.Integer)
def __repr__(self):
return f'<PageAnalysis {self.url}>'
# create tables if they don't exist yet
with app.app_context():
db.create_all()In this step we create a Flask route that accepts a URL, fetches the page, and extracts the SEO basics we need. We'll pull the <title> tag, the meta description, and count the total number of words in the visible text. The result is stored in a tiny SQLite table so you can look at past analyses later. Keeping everything in one file makes the clone easy to understand and run.
app/routes.py
import sqlite3
from flask import Blueprint, request, jsonify, render_template
import requests
from bs4 import BeautifulSoup
bp = Blueprint('seo', __name__)
# Simple SQLite helper
DB_PATH = 'seo_results.db'
def init_db():
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('''CREATE TABLE IF NOT EXISTS results (
id INTEGER PRIMARY KEY AUTOINCREMENT,
url TEXT NOT NULL,
title TEXT,
description TEXT,
word_count INTEGER,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)''')
conn.commit()
conn.close()
init_db()
def scrape(url):
resp = requests.get(url, timeout=10)
resp.raise_for_status()
soup = BeautifulSoup(resp.text, 'html.parser')
title = soup.title.string.strip() if soup.title else ''
desc_tag = soup.find('meta', attrs={'name':'description'})
description = desc_tag['content'].strip() if desc_tag and desc_tag.get('content') else ''
# Get visible text and count words
for script in soup(['script', 'style']):
script.decompose()
text = soup.get_text(separator=' ', strip=True)
word_count = len(text.split())
return title, description, word_count
@bp.route('/', methods=['GET'])
def index():
return render_template('index.html')
@bp.route('/analyze', methods=['POST'])
def analyze():
url = request.form.get('url')
if not url:
return jsonify({'error': 'URL is required'}), 400
try:
title, description, word_count = scrape(url)
except Exception as e:
return jsonify({'error': str(e)}), 500
# Store result
conn = sqlite3.connect(DB_PATH)
c = conn.cursor()
c.execute('INSERT INTO results (url, title, description, word_count) VALUES (?,?,?,?)',
(url, title, description, word_count))
conn.commit()
conn.close()
return jsonify({'url': url, 'title': title, 'description': description, 'word_count': word_count})Now we fire up the Flask development server and point our browser at the UI. The server will automatically reload when you edit the code, which is handy while you experiment. Make sure you have the virtual environment activated and Flask installed before you start.
export FLASK_APP=app
export FLASK_ENV=development
flask runWith the server running, send a quick POST request to see the JSON payload with title, description, and word count. Open the root URL in a browser to use the simple HTML form we bundled in templates/index.html. Once the basics work, you can easily add features like CSV export or a tiny chart using Chart.js to visualize word count trends. Experiment and have fun extending the clone!
curl -X POST -d "url=https://example.com" http://127.0.0.1:5000/analyze| Check | Command / Why it matters |
|---|---|
| Server starts without errors | flask run shows *Running on http://127.0.0.1:5000/* |
| POST request returns JSON with title, description, word_count | curl -X POST -d 'url=https://example.com' http://127.0.0.1:5000/analyze prints a JSON object |
| Browser UI loads the form at http://127.0.0.1:5000/ | Open the URL in a browser and see the input form |
Your mini SEO analyzer is up and running. Feel free to add more metrics (H1 count, image alt tags) or swap SQLite for Postgres as you grow. Happy hacking!