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.
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.comPrefer 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.
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)
- 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
- Python 3.11+
| 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]"seobuddy <URL> [OPTIONS]| Argument | Description |
|---|---|
URL |
Full site URL to audit (https:// recommended). Bare domains like nikitay.com are normalized to https://. |
| 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). |
# 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| 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.
Rich renders colour-coded bars and grades in real time (disable with --no-color).
| 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 |
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 |
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>.
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.).
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.
- 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
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
| 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.
| 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).
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
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.
- 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).
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
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
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 |
pip install -e ".[dev]"
pytest -q
seobuddy https://nikitay.com --depth 2
seobuddy https://nikitay.com --depth 0
seobuddy https://nonexistent.invalid # expect exit 1Expect: terminal progress, per-page lines, report *-nikitay.com-report.md with four sections, graceful failure on invalid hosts (exit 1).
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.
| 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 |
| 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 |
- 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.
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.