Skip to content

Repository files navigation

LeadLens

Audits a website for the things that actually cost a small business enquiries — no phone number, no booking, slow load, broken mobile layout — scores it out of 100, and drafts an outreach email from what it found.

Runs entirely on your machine. No paid APIs, no monthly cost, no account. One audit takes roughly 10–15 seconds.

npm run audit -- abcroofing.com
  ABC Roofing
  https://abcroofing.com/
────────────────────────────────────────────────────────
  Site score   38/100  (grade F)
  Lead score   61/100  (tier B — worth contacting)
────────────────────────────────────────────────────────
  Checked homepage + 3 internal pages
────────────────────────────────────────────────────────
  seo          ████░░░░░░   8/20
  performance  ██░░░░░░░░   4/20
  leadgen      ███░░░░░░░   7/25
  trust        ████░░░░░░   8/20
  ux           ███████░░░  11/15
────────────────────────────────────────────────────────
  Problems
   ✗ No phone number found
   ✗ No strong CTA above the fold
   ✗ No online booking option
   ✗ Slow load (8.2s)
   ! 10/12 images missing alt text
────────────────────────────────────────────────────────
  Reports written to output/abcroofing-com
  11.4s

Setup

Needs Node 18 or newer.

npm install          # also downloads Chromium for Playwright (~150MB, one time)

That's it. Nothing needs configuring and there's no key to set — the scoring overrides and the AI summary are both opt-in.

Usage

Audit one site:

npm run audit -- abcroofing.com
npm run audit -- https://abcroofing.com --sender-name "HP" --niche "roofing companies"

Audit a list:

npm run batch -- leads.txt --concurrency 3

leads.txt is one URL per line; blank lines and # comments are ignored. Batch mode also writes summary.csv and summary.md sorted by lead score, so the prospects worth contacting first are at the top.

Options

Flag Default What it does
-o, --out <dir> output Where reports go
--ai off Adds a Claude-written summary (needs an API key)
--no-shots Skips screenshots, noticeably faster
-t, --timeout <ms> 30000 Page load timeout
-q, --quiet off Suppresses progress output
--json off Prints the full result as JSON (audit only)
-c, --concurrency <n> 2 Sites at once (batch only)
--sender-name <name> Your Name Signature on the draft email
--offer <text> website improvements What you do, used in the email
--niche <text> Who you serve, e.g. "home service businesses"
--config <path> ./leadlens.config.json Scoring and crawl overrides

Output

output/abcroofing-com/
  result.json      every signal, machine-readable
  report.md        the readable report
  report.html      standalone — open it and Ctrl+P for a PDF
  email.md         the outreach draft
  screenshots/     desktop, iPhone, Pixel

report.html is fully self-contained apart from the screenshot files sitting next to it, so it prints to PDF cleanly and looks presentable if you send it to a prospect.

What it checks

Seventeen modules, all rule-based. Presence checks (phone, address, forms, booking, reviews, social) read the homepage plus the crawled internal pages, so a contact form that only lives on /contact is not reported as missing.

Lead generation (25 pts) — phone number, click-to-call, quote and contact forms, online booking, live chat, CTA strength and whether one is visible above the fold.

Trust (20 pts) — HTTPS, mixed content, security headers, testimonials and star ratings, third-party review links, physical address, social profiles, map embed.

Performance (20 pts) — load time and DOMContentLoaded from real browser timing, request count, page weight, largest script and image. No Lighthouse, no external API.

SEO (20 pts) — title, meta description, canonical, H1 count, structured data, Open Graph, robots.txt, sitemap.

UX (15 pts) — responsive viewport plus real measurements in a phone viewport (horizontal scroll, tap target sizes, body font size), image alt text, accessibility basics, broken links, favicon, typography, analytics.

Also detected but not scored: platform and framework (WordPress, Shopify, Wix, Next.js, and so on), chat and analytics vendors, cookie banner, dark mode, brand colour.

Lead score

Separate from the site score, and the more useful of the two for outreach.

The site score says how good the website is. The lead score estimates how established the business looks from public signals — paid ad pixels, a careers page, financing offers, multiple locations, review counts, a professional email domain, years in business. It's there so you spend your time on businesses that could plausibly fund a $500–$1,200 project.

The combination is what matters. A high lead score with a low site score is the sweet spot: money to spend, and obvious room to improve. The report calls this out when it sees it.

Tiers: A (70+), B (50–69), C (30–49), D (under 30).

This does not predict whether anyone will buy from you, and it isn't a judgement about the business. Plenty of excellent businesses have neglected websites. Treat it as a sorting aid and read the site yourself before pitching.

Verified vs unverified findings

A tool that tells a business owner "you have no phone number" when the number is sitting on /contact doesn't get a second email. So every negative finding carries a confidence level, and LeadLens is explicit about the difference between looked and didn't find it and couldn't look properly.

A missing thing is verified only when internal pages were crawled and the page loaded cleanly. It's unverified when the crawl found no internal pages to read, or the load degraded — a bot-block, an error status, or a near-empty document.

The distinction shows up everywhere:

  • The terminal marks unverified problems with (?) and prints a coverage line naming what was checked.
  • report.md and report.html mark them (unverified) and open with a coverage banner that names the reason the audit was limited.
  • The outreach email drops them entirely. An unverified negative never becomes a claim in an email you send. This is the one rule the whole model exists to enforce.

So a degraded audit produces a shorter email rather than a wrong one. If the email feels thin, check the coverage line — the site probably blocked the crawl.

────────────────────────────────────────────────────────
  ! Limited audit — Server returned HTTP 403 (likely bot protection).
    Unverified findings are marked (?) and kept out of the email.
────────────────────────────────────────────────────────
  Problems
   ✗ No phone number found (?)
   ✗ No online booking option (?)

Tuning the scoring

The defaults are tuned for local service businesses. If you work a different niche, drop a leadlens.config.json beside the project rather than editing source. Copy leadlens.config.example.json to start; every field is optional.

{
  "categoryWeights": { "leadgen": 40, "seo": 10 },
  "leadScore": { "establishmentWeight": 0.7, "tierThresholds": { "A": 75 } },
  "crawl": { "maxPages": 6, "timeoutMs": 5000 }
}

categoryWeights sets what each category contributes to the 100-point site score — raise leadgen if you sell conversion work, performance if you sell speed. establishmentWeight (0–1) splits the lead score between how established the business looks and how good the site is. crawl.maxPages accepts 0 to disable the crawl, which also means every absence becomes unverified, since the homepage is all that was seen.

A bad config never costs you an audit. Out-of-range and unknown values are warned about and ignored, unparseable JSON falls back to defaults, and a missing file is simply the normal case. $comment keys are allowed anywhere so you can annotate as you go.

Optional AI summary

Everything above is rule-based and needs no key. --ai adds a plain-language summary written by Claude:

cp .env.example .env      # then put your key in it
npm run audit -- abcroofing.com --ai

Only the compact signal digest is sent — booleans, numbers, and the finding messages. Never the page HTML, never the screenshots. A digest is a couple of KB, so the cost per audit is negligible and no site content leaves your machine.

If the key is missing or the call fails, the audit completes normally without the summary.

The outreach email

Every audit writes email.md with a draft built from the three highest-impact findings. It states only what the audit observed and doesn't invent statistics or promise conversion lifts.

It's a draft. Read it, add a real name, check each claim still holds, and make sure you have a lawful basis to contact the business — GDPR, PECR, CAN-SPAM and their equivalents apply to cold outreach, and the rules differ by country. Sending unedited automated emails at volume is how domains get blocked.

How it works

URL → Playwright renders the page (desktop + phone viewport)
    → capture HTML, network, timing, layout geometry, screenshots
    → axios fetches robots.txt, sitemap, favicon, samples internal links
    → shallow crawl: up to 4 linked contact/about/services pages
    → 17 check modules read that context
    → scores → lead score → recommendations → email
    → result.json, report.md, report.html, email.md
src/
  core/browser.ts       Playwright: render, measure, screenshot
  core/fetcher.ts       axios: robots, sitemap, favicon, links, shallow crawl
  core/patterns.ts      every keyword and vendor fingerprint, in one place
  checks/*.ts           one module per check, all the same shape
  checks/helpers.ts     finding builders, incl. absence() for confidence
  utils/score.ts        category aggregation → 0–100
  utils/leadscore.ts    the qualification score
  utils/config.ts       leadlens.config.json overlay, validated
  report/               markdown, html, email, optional AI, file writing
  audit.ts              the pipeline
  cli.ts                commander

Adding a check means writing one file that returns { id, title, findings, scoreParts, signals } and adding it to src/checks/index.ts. Each check declares what it's worth, and score.ts normalizes each category to its configured weight, so adding checks doesn't skew the totals.

Report a missing thing with absence(ctx, ...) rather than bad(...). That's what marks the finding unverified when the crawl couldn't confirm it, and what keeps it out of the email. Use bad() only for something you directly observed — a slow load, a broken link, an HTTP page.

Put detection keywords in core/patterns.ts rather than inline — tuning the detectors is most of the ongoing work, and it's much easier when they're in one file.

Tests

npm test

Runs an offline harness over the pure pipeline — scoring, lead score, the confidence model, both renderers, the email gate, and the config overlay — against synthetic sites. No network, no browser, about a second. It covers the cases that are painful to reproduce live: a site whose contact details exist only on /contact, a homepage-only pass, a bot-blocked degraded load, and malformed config.

The one assertion worth knowing about: it verifies that an unverified negative present in the report is absent from the email.

Scope and limits

  • Shallow crawl, not a full spider. It reads the homepage plus up to four linked contact/about/services pages. A form buried five levels deep will still be missed — but anything it reports as missing was looked for on the pages that matter, and if it couldn't crawl, the finding is marked unverified rather than asserted.
  • Detection is heuristic. Fingerprints find common vendors; a custom booking widget may be missed. False negatives are more likely than false positives.
  • Address detection covers US, UK, Canadian and Australian formats. Other regions are recognised by street type where possible, but the region label may come back blank.
  • Accessibility checks are static. They catch obvious problems. Real WCAG compliance needs manual testing with assistive technology.
  • Speed is one sample from your connection, not a lab benchmark. Treat it as directional.
  • The lead score reads a website, not a balance sheet.

Verify anything you plan to put in front of a client.

Etiquette

Reads only public pages, the way any visitor would. It doesn't log in, bypass paywalls, or touch anything behind authentication. It identifies itself in the User-Agent on the plain HTTP requests, sends a small number of requests per site, and only samples a dozen internal links rather than crawling.

Keep --concurrency low. The default of 2 is deliberate — it's polite, and Chromium is memory-hungry anyway.

Troubleshooting

browserType.launch: Executable doesn't exist — run npx playwright install chromium.

Every audit times out — some sites block headless browsers. Try -t 60000. A few will refuse regardless.

Score looks wrong for a JS-heavy site — LeadLens waits for network idle up to 6 seconds. Very slow sites may be scored before everything renders.

Slow on Windows--no-shots skips three page loads and roughly halves the time.

Licence

MIT.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages