Sales Email Template has 20,000+ free, copy-ready email pages. That number comes from four dimensions multiplied together:
- 10 email types — follow-up, cold email, meeting request, proposal follow-up, breakup, thank you, introduction, referral request, invoice reminder, apology
- 20 industries — SaaS, healthcare, finance, legal, construction, retail…
- 10 prospect roles — CEO, CTO, HR director, sales VP, founder…
- 10 scenarios — no response, clicked but no reply, after demo, after proposal, long time no contact…
10 × 20 × 10 × 10 = 20,000. The engineering challenge is making each combination read like a person wrote it, not like Hi {{first_name}}, I noticed {{company}}…. Here's how the composition works.
Each dimension owns a different part of the email
The mistake is treating every dimension as a find-and-replace variable. Instead, each one controls a different job in the email:
| Dimension | Controls |
|---|---|
| Email type | Structure and goal (ask for a meeting, close the loop, get paid) |
| Scenario | Opening line and context (why you're writing now) |
| Role | What they care about and how much detail they want |
| Industry | Vocabulary and the proof point that lands |
interface Combo { type: EmailType; industry: Industry; role: Role; scenario: Scenario }
function compose(c: Combo): { subject: string; body: string } {
const opener = scenarioOpeners[c.scenario](c); // why now
const value = roleAngles[c.role](c.industry); // what they care about
const proof = industryProof[c.industry]; // what makes it credible
const ask = typeAsks[c.type](c.role); // the one clear next step
return {
subject: subjects[c.type](c),
body: [opener, value, proof, ask].filter(Boolean).join("\n\n"),
};
}
Because each dimension writes whole sentences for its own slot, a CTO follow-up after a demo reads differently from a CEO breakup email — structurally, not just by swapped nouns.
Short beats clever
Every template aims for something you can read in a few seconds: a subject line, 3–5 short paragraphs, one ask. Long emails fail on mobile and look automated. A simple length check in the build keeps that honest:
const words = (s: string) => s.split(/\s+/).filter(Boolean).length;
for (const combo of allCombos()) {
const { body } = compose(combo);
if (words(body) > 120) warn(`too long: ${key(combo)}`);
}
Leave one line for the human
Templates are deliberately missing one thing: the detail only the sender knows — a metric from the last call, a line from the prospect's post, the next step you agreed on. The page tells you exactly which line to change. That single personalized line is what separates a template from spam.
Routing: four clicks to any page
The homepage has a four-step finder (type → industry → role → scenario) that maps to a predictable URL:
/{type}/{industry}/{role}/{scenario}
Hub pages for each type and industry link down into the combinations, so people and crawlers can reach any page without search.
Guarding against duplicate content
With 20,000 pages, near-duplicates are the main risk. A build step compares bodies across combinations with a quick similarity check and flags pairs that are too close, which usually means a dimension's copy isn't pulling its weight:
function jaccard(a: string, b: string) {
const A = new Set(a.toLowerCase().split(/\W+/)), B = new Set(b.toLowerCase().split(/\W+/));
const inter = [...A].filter((x) => B.has(x)).length;
return inter / (A.size + B.size - inter);
}
AI for custom drafts, templates for speed
Templates cover the common cases instantly. For everything else there are free AI tools — an email generator, enhancer, subject-line tool and tone adjuster. The two work together: start from a template's structure, then adjust tone or generate a fresh draft when the situation is unusual.
Takeaways
- Give each dimension its own slot and job, not just variables.
- Keep emails short and leave one line for real personalization.
- Use predictable URLs and hub pages for large combinatorial sites.
- Check for near-duplicates at build time.
Browse or copy templates at salesemailtemplate.com — no login. What's your approach to generating content from combinations without it feeling generated?
Top comments (0)