Technical SEO audit CLI — crawl a site and get terminal scores plus a Markdown report (not affiliated with seobuddy.com)
Project description
SEObuddy
Technical SEO audits from your terminal — live scores, colour-coded results, Markdown reports.
pip install seobuddy → seobuddy https://yoursite.com → done.
No clone. No build step. One package from PyPI.
See it in action
Real Rich UI in your terminal — greens for passes, yellows for warnings, reds for failures.
Full terminal + report examples
Install in seconds:
pip install seobuddy
seobuddy https://nikitay.com
Prefer an isolated CLI? Use pipx install seobuddy instead of pip.
Crawl your site, watch scores update live, then open the timestamped *-report.md file. That is the whole workflow.
Note: Open-source technical SEO audit CLI — not affiliated with the commercial product at seobuddy.com.
Documentation
Full manuals (same content as below, in more detail):
- User Manual — installation, usage, reading results, troubleshooting
- Technical Manual — architecture, modules, scoring, tests, extension points
- Report Example — sample terminal summary and Markdown report (
google.comaudit) - Publishing — PyPI releases (manual upload and GitHub Actions)
Features
- Async BFS crawler — configurable depth, same-domain internal links only, deduplicated URLs
- 11 weighted per-page categories — title, meta, Open Graph, JSON-LD, headings, content, links, images, canonical, hreflang, technical
- Site-wide checks — robots.txt validation and crawl enforcement, XML sitemap audit with coverage metrics
- Per-page and site scores — 0–100 with weighted category averages and letter grades
- Rich terminal UI — progress bars, color-coded scores, live per-page results, final summary panel
- Markdown reports — executive summary, score breakdown, page-by-page findings, prioritized recommendations
Requirements
- Python 3.11+
Quick start
| Step | Command |
|---|---|
| Install | pip install seobuddy |
| Audit | seobuddy https://nikitay.com |
| Report | Open yyyymmddhhmm-<hostname>-report.md in the current directory |
Defaults: depth 2, 5 concurrent requests, 10s timeout. Use --depth 0 for a homepage-only check.
seobuddy --help # all options
pip install -U seobuddy # upgrade
pipx install seobuddy # isolated global CLI (macOS/Linux)
Contributors — clone and editable install:
git clone https://github.com/nikitaycs50/SEObuddy.git && cd SEObuddy
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
Usage
seobuddy <URL> [OPTIONS]
Arguments
| Argument | Description |
|---|---|
URL |
Full site URL to audit (https:// recommended). Bare domains like nikitay.com are normalized to https://. |
Options
| Option | Default | Description |
|---|---|---|
--depth |
2 |
Link hops from the seed URL. 0 = seed page only. |
--max-pages |
50 |
Stop crawling after this many pages (avoids huge sites). |
--concurrency |
5 |
Maximum parallel HTTP requests during the crawl. |
--timeout |
10 |
Per-request timeout in seconds. |
--output-dir |
. |
Folder where the Markdown report is saved. |
--user-agent |
Chrome 131 (desktop) | HTTP User-Agent on every request. Default mimics Chrome for compatibility; override to identify SEObuddy if required. |
--no-color |
off | Plain terminal output (no Rich colors). |
Examples
# Default crawl (depth 2)
seobuddy https://example.com
# Homepage only (fast check)
seobuddy https://nikitay.com --depth 0
# Deeper crawl, more parallelism
seobuddy https://nikitay.com --depth 2 --concurrency 10
# Save reports to a folder
seobuddy https://nikitay.com --output-dir ./reports
# CI / logs (no colors)
seobuddy https://nikitay.com --no-color
# Custom User-Agent (default is Chrome-like)
seobuddy https://nikitay.com --user-agent "SEObuddy/0.2.0 (+https://nikitay.com)"
# Module entry point
python -m seobuddy https://nikitay.com
Verified example: nikitay.com
| URL | Result |
|---|---|
https://nikitay.com |
Works — crawls the site, prints scores, writes a report |
https://niktiay.com |
Fails — hostname does not resolve (typo: missing k in nikitay) |
After seobuddy https://nikitay.com --depth 1 you should see: startup banner, progress bar, one line per page, SITE AUDIT COMPLETE panel, and a report file in the output directory.
Understanding the terminal output
Rich renders colour-coded bars and grades in real time (disable with --no-color).
Per-page line (live)
| Colour | Score | Example |
|---|---|---|
| 🟢 Green | 80–100 | ██████████ 100/100 · category ✓ |
| 🟡 Yellow | 60–79 | ████████░░ 64/100 · category ~ |
| 🟠 Orange | 40–59 | █████░░░░░ 47/100 |
| 🔴 Red | 0–39 | ██░░░░░░░░ 2/100 · category ✗ |
████████░░ 64/100 / — title ~ meta ~ og ✓ h1 ✓
| Symbol | Meaning |
|---|---|
█ / ░ |
Score bar (colour matches score band) |
✓ ~ ✗ |
Category pass / warn / fail |
Final summary
Overall score, letter grade, category table (average score + pages OK ≥ 80), and TOP ISSUES by impact.
| Grade | Score range |
|---|---|
| A | 90+ |
| B | 80–89 |
| C+ | 70–79 |
| C | 60–69 |
| D | 50–59 |
| F | Below 50 |
The Markdown report
Filename: yyyymmddhhmm-<hostname>-report.md — example: 202606041408-nikitay.com-report.md
| Section | Contents |
|---|---|
| Executive Summary | Site score, date, seed URL, page count, duration, top 5 issues |
| Score Breakdown | All categories with scores and pages OK |
| Page-by-Page Analysis | Collapsible <details> per URL (findings + suggestions) |
| Recommendations | Prioritized, deduplicated action items |
Open in VS Code, Cursor, GitHub, or any Markdown viewer that supports HTML <details>.
What SEObuddy checks
Page score = weighted average of categories. Site score = average of all page scores.
| Category | Weight | What it looks for |
|---|---|---|
| Title | 15% | <title> present, ~50–60 characters, unique across crawled pages |
| Meta description | 10% | meta name="description", ~150–160 characters, unique |
| Open Graph | 9% | og:title, og:description, og:image, og:url |
| JSON-LD | 9% | Valid application/ld+json, schema type, required fields |
| Headings | 9% | Exactly one H1, logical heading order (no skipped levels) |
| Content | 15% | Word count (300+ tiers), text vs HTML ratio (target ≥ 15%) |
| Links | 9% | Internal links reachable (HEAD/GET probes), descriptive anchor text |
| Images | 9% | Non-empty alt on images, lazy loading where applicable |
| Canonical | 5% | link rel="canonical" present and consistent |
| Hreflang | 5% | link rel="alternate" hreflang tags, reciprocity across crawled pages |
| Technical | 5% | Viewport meta, HTTPS, reasonable URL length and structure |
Site-wide (reported separately from per-page weighted scores):
| Check | What it looks for |
|---|---|
| Robots.txt | Fetchable file, valid rules, crawl respects Disallow for your User-Agent |
| Sitemap | XML sitemap reachable, valid structure, coverage vs crawled URLs |
Content tiers: <300 words → 0; 300–499 → 60; 500–799 → 80; 800+ → 100 (combined with text/HTML ratio).
Links check: up to 20 unique internal URLs probed per page; penalizes generic anchors (“click here”, “read more”, etc.).
Auditing large sites (e.g. Google)
Mega-sites expose thousands of locale and redirect URLs. SEObuddy caps crawls at --max-pages (default 50), dedupes by path (not query), skips utility routes (/ml, /intl/…, policies), drops non-crawlable redirect targets, and truncates long paths in the terminal. For Google, use --depth 0 or --max-pages 10.
Crawl behavior
- BFS with path-based dedup; seed at depth
0, enqueue links only ifdepth < config.depth - Same domain only — external links are ignored (
www.stripped when comparing netloc) - Skips:
mailto:,tel:,javascript:,data:, fragments-only, non-HTML, duplicate paths,/cdn-cgi/,/ml, long query strings - Redirects — followed up to 5 hops; non-crawlable final URLs are not audited
- Per page: URL, status, headers, HTML body, fetch time (ms); pages yielded as they complete for live UI
Architecture
SEObuddy is a Python 3.11+ package (Hatchling, src/ layout) with a Typer CLI:
- Validates and normalizes the seed URL.
- BFS-crawls same-domain HTML with httpx (async).
- Audits each page with BeautifulSoup + lxml and eleven pluggable checks, plus site-wide robots.txt and sitemap checks.
- Renders Rich terminal UI and writes a Markdown report.
flowchart LR
CLI[cli.py] --> Crawler[crawler.py]
Crawler -->|PageData| Auditor[auditor.py]
Auditor -->|PageAudit| Display[display.py]
Auditor -->|SiteAudit| Report[report.py]
Checks[checks/*] --> Auditor
Entry points
| Mechanism | Target |
|---|---|
Console script seobuddy |
seobuddy.cli:app |
python -m seobuddy |
__main__.py → app() |
asyncio.run() wraps _run_audit from the Typer command. Connection failures on the seed fetch exit with code 1.
Core data models (models.py)
| Type | Purpose |
|---|---|
AuditConfig |
CLI settings (depth, concurrency, timeout, paths, UA) |
PageData |
Raw crawl result: URLs, status, headers, HTML, timing |
CheckResult |
Category outcome: score, weight, status, findings, suggestions |
PageAudit |
One page’s checks + weighted score |
SiteContext |
Cross-page state: titles, metas, canonicals, fetched URLs |
SiteAudit |
Full run: pages, timing, site_score |
CheckStatus: pass | warn | fail (from score thresholds in base.status_from_score).
URL layer (url_utils.py)
normalize_url— http/https only, lowercase host, strip fragment;Nonefor non-crawlable pathsis_crawlable_path— skips/cdn-cgi/(e.g. Cloudflare email protection)same_domain— compares netloc withwww.stripped
Crawler (crawler.py)
AsyncCrawler batches queue waves up to concurrency with asyncio.Semaphore. GET with redirect following; ConnectError propagates on seed failure; other HTTP errors may yield status_code=0. Link extraction from <a href> when depth allows and response is HTML.
Auditor (auditor.py)
- Parse HTML with BeautifulSoup + lxml.
- Non-HTML or error status: stub checks except technical (URL/headers still audited).
- Sync checks: title, meta, opengraph, jsonld, headings, content, images, canonical, hreflang, technical.
- Async
links.check_asyncwith sharedhttpxclient. weighted_page_score(results)andpath_display(final_url).
Checks update SiteContext for cross-page deduplication (titles, meta descriptions).
Scoring helpers (checks/base.py)
clamp_score,weighted_page_score,letter_gradeaggregate_category_scores— per-category mean + pages_ok (≥ 80)top_issues— impact = weight × (100 − category_avg)collect_recommendations— deduplicated suggestions by impact
Project structure
SEObuddy/
├── LICENSE
├── pyproject.toml
├── README.md
├── docs/
│ ├── USER_MANUAL.md
│ ├── TECHNICAL_MANUAL.md
│ ├── REPORT_EXAMPLE.md
│ └── PUBLISHING.md
├── scripts/
│ └── publish-to-pypi.sh
├── src/seobuddy/
│ ├── __init__.py # __version__
│ ├── __main__.py
│ ├── cli.py
│ ├── crawler.py
│ ├── auditor.py
│ ├── models.py
│ ├── url_utils.py
│ ├── site_resources.py
│ ├── display.py
│ ├── report.py
│ └── checks/
│ ├── base.py
│ ├── title.py … technical.py, hreflang.py
│ ├── robots_check.py, sitemap_check.py
└── tests/
├── conftest.py
├── helpers.py
└── test_*.py
Development
pip install -e ".[dev]"
pytest -q
| Test file | Coverage |
|---|---|
test_checks_*.py |
Individual check logic |
test_checks_base.py |
Grading and weighted scores |
test_auditor.py |
Full page audit integration |
test_crawler.py |
URL normalize, link extract, mock transport BFS |
test_report.py |
Filename format and report sections |
Manual verification
pip install -e ".[dev]"
pytest -q
seobuddy https://nikitay.com --depth 2
seobuddy https://nikitay.com --depth 0
seobuddy https://nonexistent.invalid # expect exit 1
Expect: terminal progress, per-page lines, report *-nikitay.com-report.md with four sections, graceful failure on invalid hosts (exit 1).
Extension points
New check: add checks/newcheck.py, register weight in CATEGORY_WEIGHTS / CATEGORY_ORDER / CATEGORY_LABELS, wire in auditor.py, add tests.
Crawl rules: extend _SKIP_PATH_PREFIXES in url_utils.py or adjust BFS depth semantics in AsyncCrawler.crawl.
CI / headless: --no-color and a dedicated --output-dir for artifacts.
Known limitations (v0.2)
| Area | Limitation |
|---|---|
| JavaScript rendering | Static HTML only (no browser execution) |
| Rate limiting | User-controlled via --concurrency only |
| Hreflang | HTML <link> tags only; no HTTP header alternates |
| Sitemap | Audited for coverage; not used to discover crawl URLs |
| Authentication | No logged-in page support |
Errors and troubleshooting
| Situation | Behavior | Exit code |
|---|---|---|
| Successful audit | Report written, summary shown | 0 |
| Bad URL format | Clear error message | 1 |
| Host unreachable (DNS, connection) | Could not connect to host |
1 |
| Request timeout | Error message | 1 |
| Problem | What to try |
|---|---|
command not found: seobuddy |
PyPI: pip install seobuddy or pipx install seobuddy. Dev: activate venv and pip install -e . |
externally-managed-environment (Homebrew Python) |
Use pipx install seobuddy, or a venv — avoid pip install into system Python |
Could not connect to host |
Check URL spelling, browser/curl, increase --timeout |
| Low content score on SPAs | Only HTTP HTML is analyzed, not client-rendered DOM |
| Unexpected extra pages | Some sites inject CDN paths; /cdn-cgi/ is skipped |
Privacy and etiquette
- Only requests URLs you point it at, within same domain and depth you set.
- Default User-Agent mimics Chrome desktop so fewer sites block the crawler; set
--user-agentexplicitly if your policy requires an identifiable bot string. - Crawl respects robots.txt
Disallowrules for your User-Agent; use reasonable depth and concurrency on live sites.
License
MIT License — see LICENSE.
Copyright © 2026 NikitaY.com. Created by NikitaY.com.
This project is a technical SEO audit CLI and is not affiliated with seobuddy.com.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file seobuddy-0.2.0.tar.gz.
File metadata
- Download URL: seobuddy-0.2.0.tar.gz
- Upload date:
- Size: 50.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
05bbf3763e7d863ce6a5c304b465b3e7a3173bf3f7b86670720810a2aaab1ab7
|
|
| MD5 |
1ec844984b44122e0390272eb0fe9e45
|
|
| BLAKE2b-256 |
6275317958238ced5347d81f07d7f9d49dcdd7ca4914b4327f60d92274187d6d
|
Provenance
The following attestation bundles were made for seobuddy-0.2.0.tar.gz:
Publisher:
publish-pypi.yml on nikitaycs50/SEObuddy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
seobuddy-0.2.0.tar.gz -
Subject digest:
05bbf3763e7d863ce6a5c304b465b3e7a3173bf3f7b86670720810a2aaab1ab7 - Sigstore transparency entry: 1720223403
- Sigstore integration time:
-
Permalink:
nikitaycs50/SEObuddy@36fc49a233ce03293bc55ddb76a1b3953a69b674 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/nikitaycs50
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@36fc49a233ce03293bc55ddb76a1b3953a69b674 -
Trigger Event:
push
-
Statement type:
File details
Details for the file seobuddy-0.2.0-py3-none-any.whl.
File metadata
- Download URL: seobuddy-0.2.0-py3-none-any.whl
- Upload date:
- Size: 41.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
407ffcfee842fb77dd1fba3d8fe6ae4b626aaefda36fe1e55b0856c43660a57c
|
|
| MD5 |
b3431ce462c49e187c09868183bd77a1
|
|
| BLAKE2b-256 |
062a28b1029a1e6b5e5a1e2590501712ce27ceed5ec60154784ab24ddc7a144c
|
Provenance
The following attestation bundles were made for seobuddy-0.2.0-py3-none-any.whl:
Publisher:
publish-pypi.yml on nikitaycs50/SEObuddy
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
seobuddy-0.2.0-py3-none-any.whl -
Subject digest:
407ffcfee842fb77dd1fba3d8fe6ae4b626aaefda36fe1e55b0856c43660a57c - Sigstore transparency entry: 1720223513
- Sigstore integration time:
-
Permalink:
nikitaycs50/SEObuddy@36fc49a233ce03293bc55ddb76a1b3953a69b674 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/nikitaycs50
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-pypi.yml@36fc49a233ce03293bc55ddb76a1b3953a69b674 -
Trigger Event:
push
-
Statement type: