Point it at any website and get one boardroom-ready deliverable: a CEO-level Word report built entirely from measured evidence - a posture scorecard, real browser-measured Core Web Vitals, severity-ranked findings that cite the exact page and element, and a prioritized plan of action. Retargeting is a one-line change.
Website Analyzer is a professional site-assessment engine built as a Claude Code project. It combines a deterministic, pure-standard-library scanner suite (14 registered scanners, 10 scorecard categories, zero third-party dependencies) with an automated headless-browser evidence tier and a deterministic Word report builder. Every claim in the final report traces back to a measurement: a header that was actually fetched, a DNS record that was actually resolved, a paint metric a real browser engine actually recorded. Nothing is estimated, nothing is eyeballed, and anything the tool could not measure is labeled exactly that.
- What you get
- How it works
- The measurement engine
- Automated rendered evidence
- Prospect triage: scoring many sites at once
- Evidence discipline
- Install
- Usage
- Command-line reference
- Tests and CI
- Scope and safety
- Project structure
Every run produces one deliverable in planning/, named by the target's domain so runs against different sites never overwrite each other:
| File | What it is |
|---|---|
planning/<slug>_Executive_Report.docx |
The CEO-level Word report, designed as a board document: a cover page with the measured posture and contents, the bottom line as a quotable statement, an executive summary of strengths and weaknesses, a scorecard with per-category score bars, a Core Web Vitals panel, a Key dates panel (certificate and domain renewal dates, domain age), severity-ranked findings that enumerate every affected page, a prioritized plan of action, quick wins, and an evidence appendix with highlighted header snippets and screenshots. |
Working artifacts (scan JSON, digest, draft data, history ledger, rendered DOM snapshots, screenshots) land under planning/_evidence/ and are internal, not deliverables.
<slug> is the host with the scheme and any leading www. removed and dots turned into hyphens (example.com becomes example-com).
The review covers Content, Design (measured signals plus browser evidence), Accessibility, Navigation and IA, SEO and Technical, Performance and delivery architecture, Privacy and tracking, and a passive Security posture pass at both the host level (TLS, security headers, cookie flags, email authentication, information disclosure) and the page level (Subresource Integrity, form-action downgrades, inline handlers).
TARGET.txt ──> discovery ──> scanner suite ──> headless-browser capture ──> re-scan ──> draft ──> executive report
(sitemap (14 passive (rendered DOM + (consumes (measured (deterministic
+ nav) scanners) vitals + contrast) evidence) data file) docx builder)
One command (run_review.py) executes the whole evidence pass:
- Discovery reads the sitemap and homepage navigation and proposes a representative review set (homepage, section landings, deep pages, legal pages). It fetches only the homepage and sitemaps, never the whole site.
- The scanner suite measures every page and the host once, sharing a single fetch per URL across all scanners, and rolls the results into a per-category scorecard.
- Automated rendered capture launches a locally installed Chrome or Edge headless, snapshots the browser-built DOM of every client-rendered page, and measures Largest Contentful Paint, Cumulative Layout Shift, Total Blocking Time, and WCAG 1.4.3 contrast on every scanned page.
- A re-scan (near-free, inside the same per-run fetch cache) upgrades the inconclusive static verdicts to measured
rendered_domverdicts and grades the captured vitals against the published Core Web Vitals thresholds. - The report data drafter turns the scan JSON into a first-draft report data file: measured scorecard rows, aggregated findings, an auto-derived executive summary (strengths, weaknesses, bottom line) and a prioritized action plan, every item traceable to a check.
- The report builder renders the final Word document. Formatting is owned entirely by the builder, so the report looks identical on every run regardless of the target or who invokes it.
The scorecard rolls each category into a posture band (Strong, Adequate, Weak, Poor, or Not measured) from its own pass/warn/fail checks, plus an overall band. It is a transparent aggregation of measured checks, not an invented benchmark, and the raw counts always travel with it.
The review never relies on a language model eyeballing headers or markup. A deterministic, strictly passive scanner suite runs first and writes its results to planning/_evidence/<slug>_scan.json (plus a readable <slug>_scan_summary.md). Each check carries a pass, warn, fail, or info verdict and a short note that findings cite directly. The suite is pure Python standard library: no requests, no BeautifulSoup, no Playwright, nothing to install.
| Tool | Measures (passively) |
|---|---|
scan_http_security.py |
HTTPS redirect, HSTS, CSP with full directive analysis (Report-Only delivery, wildcard script origins, script-scoped unsafe-inline/eval), clickjacking protection, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, cookie flags incl. SameSite, version banners, security.txt (RFC 9116) |
scan_tls.py |
Negotiated TLS protocol, HTTP/2 via ALPN on the same handshake, certificate issuer and days to expiry, hostname coverage, CAA issuance records, legacy TLS 1.0/1.1 probe |
scan_dns_email.py |
SPF, DMARC policy, DKIM across 26 documented selector families probed in parallel, MX, DNSSEC, MTA-STS (record plus policy mode), TLS-RPT, BIMI over DNS-over-HTTPS, plus domain registration and expiry dates via RDAP (the JSON successor to WHOIS) as ungraded conversation-starter facts |
scan_crawl.py |
robots.txt incl. site-wide Disallow detection, sitemap reachability, apex vs www canonicalization |
scan_crux.py |
Real-user field data from the Chrome UX Report API: origin p75 LCP, CLS, and INP graded against the published Core Web Vitals thresholds (needs CRUX_API_KEY or GOOGLE_API_KEY; reports honestly when absent or when the origin lacks traffic) |
scan_seo.py |
Title and meta-description length, canonical, viewport, robots meta, heading hierarchy, Open Graph, Twitter cards, JSON-LD, hreflang, image alt |
scan_accessibility.py |
Document language and title, image alt, form labels, heading order, landmarks, link text, positive tabindex, empty buttons, viewport zoom restrictions (WCAG 1.4.4) |
scan_links.py |
Broken links (404/410/5xx), redirects, access-restricted links, in-page anchors pointing at missing ids, mixed content |
scan_performance.py |
HTML transfer size, static resource-weight floor, render-blocking head scripts, third-party origins, gzip/brotli compression, per-asset caching lifetimes, redirect chains |
scan_readability.py |
Flesch Reading Ease, Flesch-Kincaid grade, average sentence length on visible text, with listing-page noise suppression |
scan_privacy.py |
Third-party origins, 154 documented tracker domains grouped by function, tracking pixels, cookie-consent platform detection (20 CMP hosts) |
scan_page_security.py |
Subresource Integrity coverage on cross-origin scripts and styles, forms posting to plain HTTP from HTTPS pages, inline event handlers, target=_blank rel hygiene |
scan_design.py |
Favicon and theme-color, deprecated presentational tags, inline-style density, font families from inline and linked CSS, images without dimensions (layout-shift risk) |
scan_vitals.py |
Browser-measured LCP, CLS, TBT and WCAG 1.4.3 contrast graded against the published Core Web Vitals and Lighthouse thresholds; reports "not captured" honestly when no browser evidence exists |
Supporting tools: discover_pages.py (scoping helper), crawler.py (opt-in polite crawler: robots.txt compliant including Crawl-delay, strictly serial with a per-request delay, hard 500-page ceiling, resumable state), capture_rendered.py (the automated browser tier, below), draft_report_data.py (scan JSON to report data), run_review.py (the one-command pipeline), and build_exec_report.py (the deterministic docx builder).
Analyst-grade output handling. Identical findings repeated across pages (a template-level defect) collapse into one entry naming every affected page, so the report says "missing landmarks on 12 pages" once instead of twelve times, and severe findings enumerate every affected URL with no truncation. Each run appends to a per-target history ledger, diffs itself against the previous run (new vs resolved issues), and the digest shows a trend of the last five runs with any overall-band movement, so a fix-and-rescan loop shows progress explicitly.
Static analysis cannot see what JavaScript builds, and most modern sites build a lot. This is where site scanners usually either lie (reporting an empty SPA shell as a clean page) or give up. Website Analyzer does neither.
The suite detects client-rendered pages and marks their structural checks inconclusive rather than falsely clean. Then capture_rendered.py closes the gap automatically:
- It finds a locally installed Chrome or Edge (standard install paths, then PATH;
REVIEW_BROWSERoverrides) and drives it headless over the Chrome DevTools Protocol, using a minimal RFC 6455 WebSocket client written on raw standard-library sockets. Zero dependencies survives contact with a real browser. - For every client-rendered page it captures the browser-built DOM, and the next scan runs every structural scanner against that rendered document, stamping the verdicts
evidence_source: rendered_dom. A planted image without alt text that no static scanner on earth could see gets caught and cited. - For every scanned page it measures LCP, CLS, and TBT with buffered PerformanceObserver entries and samples WCAG 1.4.3 contrast with a computed-style walk (the axe-core approach), grading everything against the published Core Web Vitals and Lighthouse thresholds. Real-user CrUX field data is preferred when available; lab capture fills in when it is not.
run_review.pyorchestrates it end to end: scan, capture, re-scan, draft, in one command, with the capture page set capped and every dropped page named. Snapshots refresh on every run, so rendered evidence never goes stale.- No browser installed? The run says so, plainly, and the inconclusive verdicts stand. Nothing is guessed. Performance numbers always stay measured from the real network transfer, never simulated from a snapshot.
The manual capture path (tools/CAPTURE.md) remains available for pages where a cookie overlay must be dismissed before capture, and a manual capture is merged with, never clobbered by, the automated one.
The full pipeline produces one deep report per site. The inverse job - sweeping many company sites to find the few worth a closer look - is what tools/triage.py does. It runs a static, homepage-only, strictly passive pass over a list of domains, ranks them worst-posture-first (a worse measured posture is a stronger candidate for a review), and gives each site a single measured "why to reach out" hook drawn from the same checks the full report uses.
# put one URL per line in sales/prospects.txt (git-ignored), then run:
python .claude/skills/review-site/tools/triage.py
# or score domains directly, no file needed:
python .claude/skills/review-site/tools/triage.py acme.com globex.com
It writes a ranked sales/triage_results.csv (for a spreadsheet or CRM) and a sales/triage_results.md (for a quick read), and prints the ranked table:
# Domain Posture Score Door-opener
1 neverssl.com Weak 0.43 Missing baseline security headers (HSTS/CSP/clickjacking)
2 example.com Adequate 0.81 Homepage served over plain HTTP with no redirect to HTTPS
3 www.python.org Adequate 0.83 Weakest measured area: security headers (Weak)
The sweep is serial with a polite delay (one homepage visit per site), so it stays light on every target. Unreachable sites become a flagged row rather than aborting the batch. The sales/ directory is git-ignored, so prospect lists and results never enter version control. The triage score and a site's eventual full-report score come from the identical scoring engine, so a triage sweep never contradicts the report you later hand the client.
The rules that make the output trustworthy, enforced by code and tests rather than good intentions:
- Every finding cites its evidence. The page URL, the exact element or header, and the scanner check it came from. No unsourced claims survive into the report.
- Nothing unmeasured is reported. No fabricated scores, no invented benchmarks, no competitor numbers. A category with no evidence is "Not measured", not a guess.
- Inconclusive is not clean. A client-rendered page without a snapshot keeps its inconclusive verdicts, stated as such.
- One scanner failing never aborts the run, and a scanner that cannot reach its target reports that as its result instead of raising.
- Honest degradation everywhere. Missing CrUX key, origin absent from the dataset, no browser installed, a page that never fires its load event: each has an explicit, tested code path that names the limitation.
- No silent truncation. The one capped list, the capture page set, names every page it dropped; findings are never capped and enumerate every affected page.
Requirements: Python 3.10+ and, for the Word report only, two packages
(python-docx, plus matplotlib for the quarterly-trend charts), pinned in
requirements.txt:
pip install -r requirements.txt
The scanner suite itself needs nothing beyond the standard library, so requirements.txt covers only the report builder and its tests. The automated browser tier uses any locally installed Chrome or Edge; without one, everything else still runs.
Clone and enter:
git clone https://github.com/lenamonj/web-site-analyzer.git
cd web-site-analyzer
This is a Claude Code project: open Claude Code in the folder and the review-site skill, output contract, and permission allowlist are picked up automatically. The evidence pipeline also runs standalone from any terminal (see below), no Claude Code required.
-
Set the target. Open
TARGET.txtand put your URL on the line beginning withhttp:https://www.example.com -
Run the review inside Claude Code:
/review-siteOr ask directly: "Run the website review against the URL in TARGET.txt and build the executive report."
-
Collect the deliverable from
planning/.
To retarget, edit the one URL line in TARGET.txt and run again. A URL given in chat ("run the review against https://another.com") overrides TARGET.txt for that run.
The full measured-evidence pass runs without Claude Code:
python .claude/skills/review-site/tools/run_review.py # reads TARGET.txt
python .claude/skills/review-site/tools/run_review.py https://example.com # explicit target
This discovers pages, scans, captures rendered evidence when a browser is present, re-scans, and drafts the report data file. Any single scanner also runs alone, for example python .claude/skills/review-site/tools/scan_tls.py example.com.
| Command | Flags |
|---|---|
run_review.py [url] |
--crawl N use the polite crawler for discovery with an N-page budget, --fresh restart a resumable crawl, --no-browser skip the rendered capture |
triage.py [domains...] |
--file list.txt read domains from a file (default sales/prospects.txt), --delay S seconds between sites |
capture_rendered.py [url] |
--pages N raise or lower the capture cap (default 15), --browser PATH explicit browser binary |
scan_site.py [url] [extra pages...] |
scan an explicit page set |
discover_pages.py [url] |
propose the in-scope review set |
build_exec_report.py <data.json> <out.docx> |
render the report from a data file |
Environment (via env or a git-ignored .env at the repo root): CRUX_API_KEY or GOOGLE_API_KEY for Chrome UX Report field data, REVIEW_BROWSER to pin the browser binary.
Three offline suites, no network, 481 tests total across the scanner, builder and chart suites; the whole gate runs in about 15 seconds on a laptop, most of it the subprocess checks that drive every CLI:
cd .claude/skills/review-site/tools
python -m unittest test_review_tools # 419 tests: parsers, graders, tool contract, pipeline, capture
cd ..
python -m unittest test_exec_report # 54 tests: the docx builder (needs python-docx)
python -m unittest test_report_charts # 8 tests: the quarterly-trend chart renderer (needs matplotlib)
The scanner suite also serves a fixture website on 127.0.0.1 from inside one test class and runs the whole pipeline against it over real sockets (discovery, every scanner, the digest and the report draft, with only DNS and TLS stubbed because loopback has neither), then builds the Word report from that draft and checks its content invariants: every finding names a reviewed page, nothing hides behind a count, no dash characters. CI re-runs that class in the builder step, where python-docx is installed, so the report check is never skipped there.
The scanner suite drives the HTML parser, every grading function, the tool contract across the whole registry (every registered tool is swept for result shape, category stamping, and no-raise-on-network-failure), the full pipeline with stubbed network primitives, and the browser tier with crafted WebSocket bytes and a fake DevTools session (including the RFC 6455 accept-key test vector). The CrUX API call, the credential reader, and RDAP are stubbed suite-wide at import; the HTTP fetch, TLS, and DoH primitives are stubbed per test, so the suite reaches no real network and reads no real key.
GitHub Actions runs all three suites on every push across Ubuntu and Windows, on Python 3.10 and 3.13.
- The review is passive and external. It inspects what any browser or DNS resolver already receives: page HTML, response headers, cookies, TLS handshakes, and public DNS records. The browser tier loads pages exactly as a visitor would, strictly serially, with a delay between pages.
- It does not log in, submit forms, brute force paths, or port scan.
- Crawling is opt-in only, robots.txt compliant (including Crawl-delay), serial, and hard-capped at 500 pages.
- Only run this against sites you own or are authorized to assess. The review states the target and the authorization assumption when it starts.
- Sites behind a login wall or aggressive bot protection may return limited data; the report says so rather than guessing.
CLAUDE.md Project context and output contract
TARGET.txt The URL to analyze (edit this to retarget)
LICENSE MIT
.github/workflows/ci.yml All three test suites + README-count guard, ubuntu + windows, py3.10 + 3.13
.claude/
settings.json Permission allowlist for the skill
skills/review-site/
SKILL.md The review workflow and rules
build_exec_report.py Deterministic python-docx report builder
report_charts.py Quarterly-trend chart renderer (matplotlib)
test_exec_report.py Builder test suite
test_report_charts.py Trend-chart renderer suite (run in CI)
tools/ The evidence engine (pure standard library)
common.py Shared fetch (cached, gzip-aware), DoH, TLS, grading
htmlmeta.py Single-pass HTML extractor shared by all page scanners
registry.py Central tool registry (single source of discovery)
discover_pages.py Sitemap/nav page-discovery scoping helper
crawler.py Opt-in polite crawler (robots-compliant, resumable)
triage.py Bulk prospect pre-screen (ranks a domain list worst-first)
capture_rendered.py Automated headless-browser capture (DevTools protocol)
scan_http_security.py ... scan_vitals.py The 14 registered scanners (see table above)
scan_site.py Orchestrator + scorecard, writes the evidence JSON
draft_report_data.py Drafts report data incl. executive summary and action plan
run_review.py One command: discover, scan, capture, re-scan, draft
test_review_tools.py Offline scanner suite (419 tests)
CAPTURE.md Manual browser-capture reference (fallback path)
planning/
_evidence/ Scan JSON, digests, ledgers, rendered snapshots (internal)
sales/ Prospect lists and triage output (git-ignored, never committed)
- All generated output avoids em dashes and en dashes by design.
- To rebrand the Word report, change the single
ACCENT_HEXconstant near the top ofbuild_exec_report.py. - Findings never carry fabricated metrics, scores, or vulnerabilities. Anything not measured is labeled as such.