Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions app/api/admin/lead-campaign/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { recordLead } from "@/lib/marketing";
import {
campaignSubject,
hireUrlFor,
isStrongScore,
selectRecipients,
type LeadRow,
type Segment,
Expand Down Expand Up @@ -111,6 +112,7 @@ async function loadAudience(): Promise<LeadRow[]> {
reportToken: (r.share_token as string | null) ?? null,
score: card.score,
scoreLabel: card.label,
kind: card.kind,
scaleHint: card.scaleHint,
topIssues: issuesByAudit.get(String(r.id)) ?? [],
isCustomer: customerEmails.has(email),
Expand Down Expand Up @@ -218,6 +220,7 @@ export async function POST(req: NextRequest) {
reportUrl: `${base}/r/${row.reportToken}`,
hireUrl: hireUrlFor(row, base),
isCustomer: row.isCustomer,
strong: isStrongScore(row),
}),
unsubscribeToken: contact.unsubscribe_token as string,
});
Expand Down
7 changes: 5 additions & 2 deletions lib/email.ts
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,8 @@ export function leadCampaignEmailHtml(input: {
reportUrl: string;
hireUrl: string;
isCustomer: boolean;
/** Already scoring well — don't pitch a rescue at them. */
strong?: boolean;
}): string {
const issues = input.topIssues.length
? `<ul style="margin:14px 0 0;padding-left:18px;color:#9aa3b2;font-size:14px;line-height:1.8;">
Expand Down Expand Up @@ -896,8 +898,9 @@ export function leadCampaignEmailHtml(input: {
<tr>
<td style="padding:22px 32px 0;">
<p style="margin:0;font-size:14px;line-height:1.6;color:#9aa3b2;">
We also fix these directly. Tell us about the site and we'll come
back with what it takes to move that number — no obligation.
${input.strong
? `That's a strong result — most sites we scan don't get there. If you want the remaining gaps closed properly, we do that work directly.`
: `We also fix these directly. Tell us about the site and we'll come back with what it takes to move that number — no obligation.`}
</p>
</td>
</tr>
Expand Down
14 changes: 14 additions & 0 deletions lib/leadCampaign.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export type LeadRow = {
reportToken: string | null;
score: number | null;
scoreLabel: string;
/** Which dial the score is on — the two run in opposite directions. */
kind: "aeo" | "slop";
scaleHint: string;
topIssues: string[];
/** Has a profiles row — an existing relationship, not a cold lead. */
Expand Down Expand Up @@ -68,8 +70,20 @@ export function selectRecipients(
return { send, excluded };
}

/**
* A site that already scores well is the wrong audience for a "want us to fix
* it?" pitch — it reads as a canned mailshot and costs credibility with
* exactly the people most likely to be competent buyers. Note the inverted
* slop dial: strong means LOW there and HIGH for AEO.
*/
export function isStrongScore(row: LeadRow): boolean {
if (row.score === null) return false;
return row.kind === "slop" ? row.score <= 25 : row.score >= 80;
}

export function campaignSubject(row: LeadRow): string {
if (row.score === null) return `Your CrawlProof scan of ${row.host}`;
if (isStrongScore(row)) return `${row.host} scores ${row.score}/100 — the last few gaps`;
return `${row.host} scored ${row.score}/100 — want us to fix it?`;
}

Expand Down
30 changes: 30 additions & 0 deletions tests/lead-campaign.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { describe, it, expect } from "vitest";
import {
campaignSubject,
excludeReason,
isStrongScore,
hireUrlFor,
selectRecipients,
type LeadRow,
Expand All @@ -14,6 +15,7 @@ function lead(over: Partial<LeadRow> = {}): LeadRow {
reportToken: "tok123456",
score: 54,
scoreLabel: "AEO Score",
kind: "aeo",
scaleHint: "out of 100 · higher is better",
topIssues: ["Blocks GPTBot", "No structured data"],
isCustomer: false,
Expand Down Expand Up @@ -127,6 +129,34 @@ describe("campaignSubject", () => {
});
});

describe("isStrongScore / subject framing", () => {
it("does not pitch a rescue at a site that already scores well", () => {
// "want us to fix it?" to a 91/100 site reads as a canned mailshot and
// costs credibility with the most competent buyers.
const row = lead({ host: "share.google", score: 91, kind: "aeo" });
expect(isStrongScore(row)).toBe(true);
expect(campaignSubject(row)).toBe("share.google scores 91/100 — the last few gaps");
});

it("still pitches the fix at a weak score", () => {
const row = lead({ host: "acme.com", score: 40, kind: "aeo" });
expect(isStrongScore(row)).toBe(false);
expect(campaignSubject(row)).toBe("acme.com scored 40/100 — want us to fix it?");
});

it("inverts 'strong' for the slop dial", () => {
// Low slop is good; high slop is bad. The same number flips meaning.
expect(isStrongScore(lead({ score: 12, kind: "slop" }))).toBe(true);
expect(isStrongScore(lead({ score: 12, kind: "aeo" }))).toBe(false);
expect(isStrongScore(lead({ score: 90, kind: "slop" }))).toBe(false);
expect(isStrongScore(lead({ score: 90, kind: "aeo" }))).toBe(true);
});

it("is never strong without a score", () => {
expect(isStrongScore(lead({ score: null }))).toBe(false);
});
});

describe("hireUrlFor", () => {
it("prefills the form and tags the source", () => {
const url = new URL(hireUrlFor(lead({ host: "acme.com", email: "p@x.com" }), "https://crawlproof.com/"));
Expand Down
Loading