Skip to content
Back to skills

Set Up Lead Capture

ASecurity

Use when adding or hardening a lead, signup, contact, newsletter, or waitlist form on a public page — the form contract (labels, autocomplete, error and success states), a single destination seam (form service / serverless function / own API), layered spam defenses (honeypot, time-trap, escalation to Turnstile), consent at the point of capture, and double opt-in. Framework-agnostic.

  • 5 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 9, 2026
developmentrustgobashapifrontend

Works with

  • cli
  • api

Security analysis

A100/100

Pro scans all 2 files and shows the line behind each finding

Scanned October 9, 2026

npx -y skills add VelimirMueller/vlm-code-context-mcp --skill set-up-lead-capture --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Set Up Lead Capture?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Set Up Lead Capture
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/velimirmueller-set-up-lead-capture/badge)](https://www.skillsdirectory.com/skills/velimirmueller-set-up-lead-capture)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: set-up-lead-capture
description: Use when adding or hardening a lead, signup, contact, newsletter, or waitlist form on a public page — the form contract (labels, autocomplete, error and success states), a single destination seam (form service / serverless function / own API), layered spam defenses (honeypot, time-trap, escalation to Turnstile), consent at the point of capture, and double opt-in. Framework-agnostic.
---

# Set Up Lead Capture

A landing page exists to convert; this form is the conversion. The skill wires it with
one destination seam, invisible-first spam defenses, and consent that would survive an
audit.

## 1. Gate — is this a public page?

Run the gate from `../_shared/page-types.md`. Authenticated app forms (settings,
checkout, CRUD) are the other catalogue's job: `skills/frontend/set-up-forms`
(schema-first, mutation-wired). This skill owns the *public, anonymous* form.

## 2. Audit current state

```bash
curl -s "$URL" -o /tmp/page.html
grep -oiE "<form[^>]*" /tmp/page.html              # how many forms, where do they post?
grep -ciE "<label" /tmp/page.html                   # labels present?
grep -ciE "autocomplete=" /tmp/page.html
grep -ciE "type=\"checkbox\"[^>]*consent|consent[^>]*checkbox" /tmp/page.html
```

Findings to look for: forms posting directly to a vendor URL scattered per page (no
seam), missing labels, no spam defense, no consent checkbox, success state that dumps
PII into the URL. (As in `../set-up-seo/SKILL.md`: the greps are coarse presence checks on double-quoted
attributes — the consent pattern will also match prose mentioning consent and checkbox;
read the page, don't trust the counts.)

## 3. The form contract

```html
<form method="post" action="/api/leads">
  <label for="lead-email">Work email</label>
  <input id="lead-email" name="email" type="email" autocomplete="email"
         required aria-describedby="lead-email-err">
  <p id="lead-email-err" class="field-error" hidden></p>

  <!-- honeypot: bots autofill it; humans never see it. No aria-hidden — it would
       strand a focusable input in a hidden subtree; the label warns anyone who lands here -->
  <div class="hp">
    <label for="lead-company2">Leave this field empty</label>
    <input id="lead-company2" name="company2" type="text" tabindex="-1" autocomplete="off">
  </div>
  <!-- time-trap: must be stamped per page LOAD (SSR per request, or inline JS) —
       a build-time value on a static page never trips the check -->
  <input type="hidden" name="form_ts" value="{{render_timestamp}}">

  <label class="consent">
    <input type="checkbox" name="consent" required>
    I'd like to receive the newsletter — see the <a href="/privacy">privacy policy</a>.
  </label>

  <button type="submit">Get the guide</button>
  <p role="status" class="form-feedback"></p>
</form>
```

```css
.hp { position: absolute; left: -9999px; }   /* off-screen: the laziest bots skip display:none; the field existing is the real trap */
```

- Minimal fields — each extra field costs conversions and widens PII. Often email alone.
- Real `<label>`s; `autocomplete` for WCAG 1.3.5 input purpose, `type="email"` for the
  right mobile keyboard.
- Error state: inline, per field, announced (`aria-describedby` — the field-level
  channel). Success state: the `role="status"` element — the form-level channel —
  with visible confirmation **that says what happens next** ("Check your inbox to
  confirm"). One message per channel; don't announce the same thing twice.

## 4. The destination seam

All forms post to **one** endpoint/handler; the vendor (CRM, list provider, webhook)
lives behind it, named in one place. Shapes and trade-offs: `./capture-patterns.md`.
The handler's contract — whatever implements it:

1. Reject silently (normal success response, record dropped) if `company2` is non-empty,
   or if `form_ts` is present and `now − form_ts < 3 s`. A missing `form_ts` (JS off on
   a static page) degrades gracefully — the honeypot still guards.
2. Validate the email server-side; honest inline error for real mistakes.
3. Rate-limit by IP.
4. Store: `{ email, consent: true, consent_text_version, submitted_at, source_page }`.
5. Trigger the double-opt-in confirmation; the address is *used* only after the click.

## 5. Spam defenses — escalate, invisible first

The honeypot is free, invisible, and static-safe — it goes in always. The time-trap
needs a **per-load** `form_ts` (SSR per request, or a one-line inline script setting
`Date.now()` on load); on a fully static page a build-time timestamp never trips the
check, so treat the time-trap as an upgrade where rendering allows, not a given.
Escalate to Cloudflare Turnstile (managed/invisible mode) only when measured spam
pressure demands; an interactive challenge is the last resort, because every challenge
costs real conversions. Rationale and rejection etiquette: `./capture-patterns.md`.

## 6. Consent + double opt-in

- Checkbox **unticked** by default; specific text; policy linked. Pre-ticked or bundled
  consent isn't consent.
- Record consent (timestamp + text version) with the lead.
- Double opt-in: store unconfirmed → confirmation email → only confirmed addresses enter
  the list. Established proof-of-consent practice in the EU. (Engineering guidance, not
  legal advice — a regulated project encodes its counsel's rules via
  `../audit-copy-compliance/SKILL.md`.)

## 7. Verify

- Valid submit → success state, record stored with consent fields, confirmation mail
  flow triggers.
- Fill the hidden `company2` field → normal success response, **no** record.
- Submit within 3 s of load → no record (on per-load `form_ts` setups).
- Bad email → inline error names the field.
- View the page with JS disabled: the form is present and labeled (it's in the HTML).

## References
- ./capture-patterns.md — destination shapes, spam escalation, silent rejection, consent recording, PII minimization, when to deviate.
- ../_shared/page-types.md — the gate.
- ../../frontend/set-up-forms/SKILL.md — authenticated in-app forms (schema-first, mutation-wired); use that skill there.

Files in this skill

  • SKILL.md6 KB
  • capture-patterns.md3 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.