AI 互動行銷頁實戰 — 目標九宮格 · 第 09 章
honeypot 擋機器人、UNIQUE 去重不報錯、Bearer token 後台、一鍵匯出 CSV。社群觸及是租來的,email 名單才是你自己的資產。
核心觀念
免費價值先給,
留資換加值,
承諾當場兌現。
桌布 PNG 直接給,不設牆;email 表單放在下載旁邊,用「30 天行動追蹤模板」交換。訪客已經拿到主要價值,留資是加值交易,不是門票 — 轉換才不會帶著怨氣。
<form id="signup-form" novalidate>
<input type="email" name="email" placeholder="you@example.com">
<input type="text" name="company" class="hp-field"
tabindex="-1" autocomplete="off" aria-hidden="true">
<button type="submit">送出拿模板</button>
</form>
/* honeypot:移出視野但仍在 DOM(真人看不到,機器人照填) */
.hp-field { position: absolute; left: -9999px;
width: 1px; height: 1px; opacity: 0; }
欄位越少轉換越高,所以只收 email。笨的灌名單機器人會把每個欄位都填滿 — company 有值就當機器人,但不回錯誤:假裝成功、不寫入,讓它以為得逞,不會回頭換手法。
// worker-deploy/src/lib/signup.js(節錄)
if (limited(ip)) return r(429, { error: "rate_limited" }); // 分鐘限流
if (!(await verifyTurnstile(env, body.turnstileToken, ip)))
return r(403, { error: "turnstile_failed" }); // Turnstile
if (body.company) // honeypot
return r(200, { ok: true, gift: env.GIFT_URL }); // 假成功
const email = String(body.email || "").trim().toLowerCase();
if (!EMAIL_RE.test(email) || email.length > 120)
return r(400, { error: "bad_email" });
const quota = await takeQuota(db, { kind: "signup", ip, limit: 10, day });
if (!quota.ok) return r(429, { error: "rate_limited" }); // D1 日配額
await db.prepare(
"INSERT INTO signups (email, name, created_at, ip) VALUES (?, ?, ?, ?)")
.bind(email, name || null, now, ip).run();
分鐘限流是記憶體 Map(快、零成本,但 Worker 多實例會歸零,best-effort);D1 原子配額才是繞不過的那層 — 第 08 章的教訓在便宜端點上的應用。email 先 trim 再轉小寫,Abc@Mail.com 和 abc@mail.com 才算同一人。
} catch (e) {
if (String(e).includes("UNIQUE")) { // schema:email TEXT NOT NULL UNIQUE
return r(200, { ok: true, already: true, gift: env.GIFT_URL });
}
}
// app/js/signup.js — 前端文案
already ? '你已經在名單上了 —— 模板連結照樣再給你一次:'
: '完成,Email 已登記。這是你的 30 天行動追蹤模板:'
使用者換台電腦回來再填,看到「此 email 已存在」只會困惑 — 他要的是模板,再給一次就好;外人也無法用狀態碼探測誰在名單上。誠實註記:already 欄位本身仍可被探測,若收的是敏感名單(客戶、病患、會員),連 already 都拿掉,兩種回應完全一致。
// app/admin.html — token 走標頭,不放網址
const r = await fetch(apiBase() + '/list',
{ headers: { Authorization: 'Bearer ' + t } });
// worker-deploy/src/lib/signup.js — 與 Secret 比對
export async function handleList({ db, env, bearer }) {
if (!env.ADMIN_TOKEN || bearer !== env.ADMIN_TOKEN) {
return { status: 401, body: { error: "unauthorized" } };
}
const { results } = await db.prepare(
"SELECT id, email, name, created_at, ip FROM signups ORDER BY id DESC").all();
return { status: 200, body: { signups: results, count: results.length } };
}
兩條前課就講過的鐵則:密碼只存 Worker Secret(ADMIN_TOKEN),永不出現在前端程式碼;傳輸走 Authorization 標頭 — ?token= 會被瀏覽器歷史和 CDN log 記下。
安全設計講出來就是信任感:輸入框上方直接註明「只記到這個分頁關閉為止」。
// app/admin.html — RFC 4180:欄位全包雙引號、引號翻倍、CRLF 列尾
function buildCsv(items) {
const q = (v) => '"' + String(v ?? '').replace(/"/g, '""') + '"';
const rows = [['email', 'name', 'created_at', 'ip']]
.concat(items.map((s) => [s.email, s.name || '', s.created_at, s.ip || '']));
return rows.map((r) => r.map(q).join(',')).join('\r\n') + '\r\n';
}
// 開頭加 UTF-8 BOM:Excel 直接開啟也認得中文
const blob = new Blob(['\uFEFF' + buildCsv(list)],
{ type: 'text/csv;charset=utf-8' });
稱呼裡一個逗號就能撐破整列;沒有 BOM,Excel 開起來中文全亂碼。匯出後丟進 MailerLite / Mailchimp / Brevo 的「匯入聯絡人」,你的第一波 email 行銷就有對象了。
← → 翻頁