-
-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathindex.html
More file actions
251 lines (238 loc) · 14.4 KB
/
Copy pathindex.html
File metadata and controls
251 lines (238 loc) · 14.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTML Email Checklist — html-email</title>
<meta name="description" content="A zero-dependency, self-contained checklist for shipping a cross-client HTML email. Progress saves in your browser.">
<meta name="color-scheme" content="light dark">
<!--
============================================================================
HTML Email Checklist — a self-contained companion tool for the html-email
framework. Rebuilt from the original jQuery + jQuery-UI + Modernizr +
Selectivizr + Google-Sheets version: that pulled its items from a Google
Sheets v3 feed (shut down in 2021) so it no longer loaded anything. This
version is vanilla JS, has zero external requests, embeds the checklist
content directly, and persists progress in localStorage. Open it straight
from disk; it needs no server and no network.
============================================================================
-->
<style>
:root {
--bg: #f4f4f7; --panel: #ffffff; --ink: #1f2330; --muted: #6b7280;
--line: #e5e7eb; --accent: #4f46e5; --accent-weak: #eef2ff; --ok: #16a34a;
--bar-track: #e5e7eb;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #14151a; --panel: #1e2027; --ink: #e8e9ec; --muted: #9aa0ab;
--line: #2c2f36; --accent: #8b87ff; --accent-weak: #23233a; --ok: #4ade80;
--bar-track: #2c2f36;
}
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg); color: var(--ink);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
line-height: 1.5; -webkit-font-smoothing: antialiased;
}
.wrap { max-width: 820px; margin: 0 auto; padding: 0 20px 80px; }
header.top {
position: sticky; top: 0; z-index: 10; background: var(--bg);
border-bottom: 1px solid var(--line); padding: 20px 0 14px;
}
header.top .wrap { padding-bottom: 0; }
h1 { font-size: 22px; margin: 0 0 2px; letter-spacing: -0.01em; }
.sub { color: var(--muted); font-size: 13px; margin: 0 0 14px; }
.sub a { color: var(--accent); }
.barrow { display: flex; align-items: center; gap: 14px; }
.bar { flex: 1; height: 10px; background: var(--bar-track); border-radius: 6px; overflow: hidden; }
.bar > span { display: block; height: 100%; width: 0; background: var(--accent); border-radius: 6px; transition: width .25s ease; }
.count { font-variant-numeric: tabular-nums; font-size: 13px; color: var(--muted); white-space: nowrap; min-width: 96px; text-align: right; }
.reset {
font: inherit; font-size: 12px; color: var(--muted); background: transparent;
border: 1px solid var(--line); border-radius: 6px; padding: 5px 10px; cursor: pointer;
}
.reset:hover { color: var(--ink); border-color: var(--muted); }
section.phase { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 8px 20px 14px; margin: 22px 0 0; }
.phase h2 { font-size: 13px; text-transform: uppercase; letter-spacing: 0.08em; color: var(--accent); margin: 16px 2px 4px; }
.phase h2 .n { color: var(--muted); font-weight: 400; }
ul.items { list-style: none; margin: 0; padding: 0; }
li.item { border-top: 1px solid var(--line); padding: 12px 2px; }
li.item:first-child { border-top: 0; }
label.check { display: flex; gap: 12px; align-items: flex-start; cursor: pointer; }
label.check input { margin: 3px 0 0; width: 18px; height: 18px; accent-color: var(--accent); flex: none; cursor: pointer; }
label.check .txt { flex: 1; }
li.item.done label.check .txt { color: var(--muted); text-decoration: line-through; text-decoration-color: var(--line); }
.why { margin: 6px 0 0 30px; }
.why summary { color: var(--muted); font-size: 12px; cursor: pointer; list-style: none; display: inline-flex; align-items: center; gap: 5px; }
.why summary::-webkit-details-marker { display: none; }
.why summary::before { content: "+"; color: var(--accent); font-weight: 700; }
.why[open] summary::before { content: "-"; }
.why p { margin: 8px 0 2px; font-size: 13px; color: var(--muted); max-width: 62ch; }
footer { color: var(--muted); font-size: 12px; margin-top: 28px; text-align: center; }
footer a { color: var(--accent); }
.done-note { display: none; background: var(--accent-weak); color: var(--ink); border: 1px solid var(--accent); border-radius: 10px; padding: 12px 16px; margin: 22px 0 0; font-size: 14px; }
.done-note.show { display: block; }
@media print {
header.top { position: static; }
.reset, .why summary { display: none; }
.why[open] p, .why p { display: block; }
section.phase { break-inside: avoid; border-color: #ccc; }
body { background: #fff; color: #000; }
}
</style>
</head>
<body>
<header class="top">
<div class="wrap">
<h1>HTML Email Checklist</h1>
<p class="sub">Ship a cross-client email without surprises. Progress saves in this browser only. Part of <a href="https://github.com/dcondrey/html-email">html-email</a>.</p>
<div class="barrow">
<div class="bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-label="Checklist completion"><span id="fill"></span></div>
<span class="count" id="count">0 / 0</span>
<button class="reset" id="reset" type="button">Reset</button>
</div>
</div>
</header>
<main class="wrap">
<div class="done-note" id="doneNote">All checked. Do a final seed-send and a second pair of eyes before you hit send.</div>
<div id="list"></div>
<footer>
Self-contained · no tracking · no network. Curated from the framework's documented client quirks —
see <a href="https://github.com/dcondrey/html-email/blob/master/docs/quirks.md">docs/quirks.md</a>.
</footer>
</main>
<script>
"use strict";
// Checklist content lives here, embedded (the original pulled it from a now-dead
// Google Sheets feed). Each item: [short step, why it matters]. Grouped by phase.
var PHASES = [
["Setup", [
["Pin down the single goal and primary call to action", "One email, one job. Every section should push toward the same click; a second competing CTA measurably lowers the first one's rate."],
["Write the from-name, subject, and preheader together", "They are read as one unit in the inbox. Keep the subject under ~45 chars and make the preheader add information, not repeat the subject."],
["Host every image on an absolute HTTPS URL", "Clients cannot resolve relative or localhost paths once the email leaves your machine. Use full https:// URLs on a host that allows hotlinking."],
["Decide the dark-mode strategy before building", "Declare color-scheme and supported-color-schemes so Apple Mail and Outlook stop force-inverting your colors, then opt in deliberately."],
["Set the content width to 600px", "600px is the safe maximum that fits the classic Outlook reading pane and most mobile viewports without horizontal scroll."]
]],
["Design", [
["Keep the layout single-column, or columns that stack on mobile", "Multi-column layouts must collapse to one column under ~600px. Fluid-hybrid (inline-block) columns stack even where media queries are unsupported."],
["Design for images-off first", "Outlook and many clients block images by default. The email must communicate its message and CTA with zero images loaded."],
["Give every meaningful image real alt text", "Alt text is the blocked-image fallback and the screen-reader experience. Use alt=\"\" only for purely decorative images."],
["Hold text contrast at 4.5:1 or better, and never rely on color alone", "Low contrast fails in bright light and for low-vision readers; color-only meaning fails for color-blind readers and in forced dark mode."],
["Avoid background images for anything critical", "Classic Outlook drops CSS background images (VML is fragile). Never place text you need over a background-image alone."],
["Make buttons bulletproof and at least 44px tall", "A CSS-only button has a dead click area in Outlook. Use a VML v:roundrect so the whole shape is clickable; 44px is the minimum comfortable tap target."]
]],
["Build", [
["Use the XHTML 1.0 Transitional doctype", "It gives the classic Outlook Word engine the most predictable box model; the HTML5 doctype triggers extra spacing and unit bugs there."],
["Lay out with tables, and mark every layout table role=\"presentation\"", "Tables are the only reliable cross-client layout primitive; role=\"presentation\" stops screen readers announcing them as data tables."],
["Inline your CSS, and keep any <style> block under 8KB", "Many clients strip <head> and <style>. Inline the load-bearing styles; Gmail drops the entire <style> block if it exceeds ~8KB or contains any CSS error."],
["Add MSO ghost tables and pin PixelsPerInch to 96", "Conditional ghost tables give the Word engine the fixed widths it ignores from max-width; PixelsPerInch 96 stops Outlook upscaling images at high system DPI."],
["Give every <img> width, height, alt, and display:block", "Explicit dimensions prevent reflow and Outlook overflow; display:block kills the 3px gap under images. A missing width has caused real multi-thousand-pixel overflows."],
["Add a hidden preheader with zero-width spacers", "The preheader controls the inbox preview line. Zero-width joiners after it push body text out of the preview so only your line shows."],
["Ship no JavaScript, external CSS, or web forms", "Clients strip scripts and often external stylesheets and forms for security; anything that depends on them silently disappears."],
["Keep the total HTML under ~102KB", "Gmail clips messages larger than ~102KB, hiding everything after the cut including the footer and unsubscribe link."],
["Include a working unsubscribe link and a physical mailing address", "Both are legally required (CAN-SPAM, CASL, GDPR) and expected by Gmail and Yahoo bulk-sending policy."]
]],
["Test", [
["Render across Outlook (Windows), Apple Mail, Gmail, iOS, and Android", "These engines disagree the most. Classic Outlook (Word) and Gmail (style stripping) catch the majority of real bugs."],
["Check light AND dark mode", "Dark mode inversion differs per client. Verify your colors, logos, and any pure #000/#fff survive in both schemes."],
["View it with images blocked", "Confirm the alt text, background colors, and CTA still carry the message when nothing loads."],
["Click every link and confirm the unsubscribe works", "Catch placeholder # hrefs, wrong or missing tracking parameters, and a broken or missing unsubscribe before subscribers do."],
["Send a seed test and check the spam score and auth", "A live seed surfaces rendering and deliverability issues at once. Confirm SPF, DKIM, and DMARC pass and the spam score is low."],
["Proofread the copy and every merge-tag fallback", "Typos erode trust; an empty merge tag renders as \"Hi ,\". Set default values and preview with a record that has missing fields."]
]],
["Pre-send", [
["Confirm the audience segment and the send time", "The right content to the wrong list, or at 3am local, undoes the work. Double-check the segment count and the scheduled time zone."],
["Confirm the reply-to address is monitored", "Replies to an unwatched no-reply address lose real responses and can hurt sender reputation."],
["Do a final size check under 102KB", "Late edits and pasted styles inflate size. Re-check the byte count so the footer is not clipped in Gmail."],
["Get a second person to approve the seed", "A fresh reviewer catches the subject typo, the wrong date, and the broken CTA that you have stopped seeing."]
]]
];
var KEY = "html-email-checklist:v1";
var listEl = document.getElementById("list");
var fillEl = document.getElementById("fill");
var countEl = document.getElementById("count");
var barEl = document.querySelector(".bar");
var doneNote = document.getElementById("doneNote");
var saved;
try { saved = JSON.parse(localStorage.getItem(KEY)) || {}; } catch (e) { saved = {}; }
var total = 0;
PHASES.forEach(function (phase, pi) {
var items = phase[1];
var section = document.createElement("section");
section.className = "phase";
var h2 = document.createElement("h2");
h2.innerHTML = phase[0] + ' <span class="n">' + items.length + "</span>";
section.appendChild(h2);
var ul = document.createElement("ul");
ul.className = "items";
items.forEach(function (item, ii) {
total++;
var id = "p" + pi + "i" + ii;
var li = document.createElement("li");
li.className = "item";
li.id = "li-" + id;
var label = document.createElement("label");
label.className = "check";
var box = document.createElement("input");
box.type = "checkbox";
box.id = id;
box.checked = !!saved[id];
var txt = document.createElement("span");
txt.className = "txt";
txt.textContent = item[0];
label.appendChild(box);
label.appendChild(txt);
li.appendChild(label);
if (item[1]) {
var det = document.createElement("details");
det.className = "why";
var sum = document.createElement("summary");
sum.textContent = "Why";
var p = document.createElement("p");
p.textContent = item[1];
det.appendChild(sum);
det.appendChild(p);
li.appendChild(det);
}
if (box.checked) li.classList.add("done");
box.addEventListener("change", function () {
li.classList.toggle("done", box.checked);
if (box.checked) { saved[id] = 1; } else { delete saved[id]; }
persist();
update();
});
ul.appendChild(li);
});
section.appendChild(ul);
listEl.appendChild(section);
});
function persist() {
try { localStorage.setItem(KEY, JSON.stringify(saved)); } catch (e) { /* storage may be disabled */ }
}
function update() {
var done = 0, i;
for (i in saved) { if (Object.prototype.hasOwnProperty.call(saved, i)) done++; }
var pct = total ? Math.round((done / total) * 100) : 0;
fillEl.style.width = pct + "%";
countEl.textContent = done + " / " + total;
barEl.setAttribute("aria-valuenow", String(pct));
doneNote.classList.toggle("show", done === total && total > 0);
}
document.getElementById("reset").addEventListener("click", function () {
saved = {};
persist();
var boxes = document.querySelectorAll('input[type="checkbox"]');
for (var i = 0; i < boxes.length; i++) {
boxes[i].checked = false;
var li = document.getElementById("li-" + boxes[i].id);
if (li) li.classList.remove("done");
}
update();
});
update();
</script>
</body>
</html>