- Oct 12, 2026
- 7 min read
A Multi-Site Inbound Lead Engine for a One-Person Business: The Whole Series
Earlier this year, my sites collected leads in five different ways: a KV form here, a third-party form relay there, a hosted survey, a booking tool, a database on my homelab. Two contact addresses, consent handled differently on every site, and no single list of who had written to me.
Today it’s one engine. Every form on six sites, the website audit, the ebooks newsletter and paid bookings feed the same pipeline. I wrote it up as I went: fifty-odd articles. This page is the map.
Is it an “engine”?
For the inbound half, yes. It turns a visitor who raises their hand into a recorded, consented, followed-up lead, with no manual step and no third-party form tool. It doesn’t create the visitors. Traffic still comes from content, search, and sharing posts. The engine makes sure none of that attention is wasted.
flowchart TD
A["Attract<br/>articles · free audit · ebooks · solver app"] --> C["Capture<br/>same-origin forms, no third parties"]
C --> K["Consent + confirm<br/>double opt-in, signed links"]
K --> S["Store<br/>KV with retention · homelab mirror"]
S --> L["Alert<br/>phone push, counts-only digest"]
S --> N["Nurture<br/>drips, deep report emails"]
N --> B["Book<br/>qualifier → paid call"]
The rest of this page follows those stages. Each link has a line or two on what you’ll find there.
Start here: the big picture
- From Five Lead Silos to One Lead Hub: Before and After: what six sites used before, what they use now, and what I gave up in exchange.
- Cloudflare Today, Open Source When Needed: why it runs on Pages, Workers and KV, and the exit plan for every piece.
- Architecture Decision Records for a One-Person Business: how I wrote the decisions down, with honest checklists that stopped me building the wrong thing.
- Shipping Custom Sites: The Practices I Keep: the checklist every site now follows.
Attract: lead magnets
The free website audit
- How My Website Audit Tool Works: Astro front end, Express API, shell audit phases in parallel.
- What My Website Audit Checks and How the Score Is Computed: every check and the weighted grade, limits included.
- Running Strangers’ URLs Safely:
SSRF checks, redirect and rate limits, and
execFilefor a tool anyone can point anywhere. - From Scores to Sentences: turning Lighthouse JSON into one readable diagnosis line.
- Expiring Report Links: random, short-lived report tokens instead of guessable IDs.
- Checking HTTPS Like a Visitor:
certificates, expiry, TLS versions and redirects with Node’s
tls. - Emailing a Full Website Report: Confirm First, Then Crawl: the deep report: a signed link proves the address before any crawling starts.
- A Client-Ready HTML Report Without AI: rules and a template; every sentence comes from a measured number.
- A Failed Check Is Not a Bad Score: “not checked” is different from “bad”, and my own report got it wrong.
The solver app
- Ship a Python Library as an Installable Web App with Pyodide: an open-source solver running in the browser as a PWA.
- app.domain or domain/app?: where a lead-magnet app should live, and what that means for SEO.
Capture: forms without third parties
- Contact Forms with Cloudflare Workers KV and Self-Hosted ntfy Alerts: the original hundred-line backend: store first, alert second.
- One Forms Worker for Every Site:
one Worker answering
/api/forms/*on every domain, same-origin. - Replacing a Formbricks Qualifier with My Own Form: the consulting qualifier moved into the same pipeline as everything else.
- Server-to-Server Leads: why the audit backend needs a shared key for the one kind only it may send.
- Cal.com Bookings Into My Own Pipeline: a signed webhook and an urgent alert the first time someone pays.
- A Spam Blocklist That Stores Instead of Drops: stopping scripted sales pitches with zero visitor friction; Turnstile deferred.
- One Checkbox, Two Problems: unbundling a required checkbox that also hid marketing consent.
- Testing Form States with Playwright Route Mocking: testing every message a form can show without sending a single email.
Consent and email
- Double Opt-In on Cloudflare Workers with Stateless Signed Links: HMAC links, no token table, pages mail scanners can’t trigger.
- A Do-Not-Send List That Stores No Email Addresses: suppression keyed by hashes, checked before every send.
- Mailpit vs Mailtrap vs listmonk: what each tool is actually for, and why I didn’t use listmonk.
- A Drip Email Engine in Standard-Library Python: sequences as files, a ledger that never emails twice, no dependencies.
- The Python Email Header Bug That Silently Breaks One-Click Unsubscribe:
header folding that turns
List-Unsubscribeinto something clients ignore. - Mailjet Held My Emails: the domain-ownership TXT record Cloudflare placed one level too high.
- One Sender Adapter, Two Providers: switching Mailtrap and Mailjet without code changes, quirks included.
- From Form to First Drip: Testing an Email Pipeline on Production: an allow-list and the checks at each hop.
- A Mailjet Event Webhook That Only Suppresses Permanent Failures: which bounces and complaints should stop email, and which shouldn’t.
Store and alert: the homelab side
- Self-Hosting ntfy Behind a Cloudflare Tunnel, Locked Down: deny-all by default, a write-only token for the sites.
- A Pull-Only Homelab: mirroring KV into SQLite over outbound HTTPS only, with a safety brake.
- A Weekly ntfy Digest With Counts Only: one quiet notification a week, numbers only, never an address.
- Cloudflare Error 1010 Silently Blocked My Own Homelab Alerts: Python’s default User-Agent, blocked before it reached ntfy.
Privacy and retention
- GDPR-Friendly Lead Capture for a Small EU Business: site profiles, recorded consent, double opt-in, retention and erasure.
- Retention as a KV TTL: the privacy policy’s retention periods, enforced by the platform.
- Writing the Privacy Page Found a Retention Bug: sign-ups with no expiry, fixed with retention that depends on status.
- A Contact Link Instead of an Email in Every Footer: where the address must stay, and what it cost everywhere else.
Running the sites
- One Canonical Address Per Site:
apex, www and
pages.devcollapsed onto one host, with a curl check. - Publish Future-Dated Posts on Cloudflare Pages With a Cron Worker: scheduled posts on a static site that only builds on push.
- Deploy a Direct-Upload Cloudflare Pages Site from a Git Worktree: only committed code goes live, never drafts in the folder.
- Validate Mermaid Diagrams Before You Push Them: catching broken and tangled diagrams before readers do.
Auditing my own sites
- I Ran My Website Audit on My Own Sites: 71 to 99: fix the tool first, then the sites; before and after.
- A Missing 404 Page Hid 273 Broken Links: soft 404s on Pages, and a remark plugin for GitHub-style links.
- My LCP Was Render Delay: render-blocking Google Fonts loaded twice; self-hosting them cut LCP from 3.8 s to 2.4 s.
- A Content Security Policy on Cloudflare Pages, Tested in a Real Browser:
_headers, per-path overrides, and Playwright catching violations. - Lazy-Loading the Cal.com Embed: about 1 MB of JavaScript only for visitors who scroll to the calendar.
- Checking og:image Properly: reading the real size and format of share images from their bytes.
- Share Cards as Code: cards rendered with Playwright, and why they’re JPEG, not WebP.
Debugging notes
- Debugging Notes from a Week on Cloudflare (and Docker): fourteen gotchas, each as symptom, cause and fix.
- Shell Traps While Watching Logs:
grep buffering and
pkill -fmatching its own shell. - Cloudflare Email Obfuscation Fooled My Deploy Check: why grep found no addresses on a page that had them.
- Debugging a Pasted Secret: The Key Was 65 Characters: a probe that prints a key’s length and hash prefix, never the key.
- A Bind-Mounted Repo Turns Runtime Files Into Git Files: live data written into a tracked file, and how I moved it out.
What I’d tell someone starting this
- Own the endpoint, not the vendor. Forms posting to your own domain can move anywhere later.
- Store first, everything else second. Alerts, emails and webhooks can fail; the lead must not.
- Consent is a data structure. Record what was agreed, when, and with which wording.
- Retention belongs in code. If the privacy page promises it, something must enforce it.
- Use your own tools on yourself. My audit found 279 broken links on my own sites.
If you’re a small business that wants the same thing, without a CRM subscription or a form vendor holding your list, that’s the kind of work I do. Or start with the free website audit.