I wrote about what Gmail and Outlook do to an SVG logo and a flex row a few days ago. This is the other half of the same file: the parts of an HTML email shell that are not about the branding, and where each one gets its behaviour from.
Every transactional and campaign email CogniPrep sends goes through one function:
export function wrapEmail(
bodyRows: string,
unsubscribeUrl: string | null,
options: { reason?: string; preheader?: string } = {}
): string
Body rows in, complete document out. Three things in that signature are load bearing.
bodyRows is <tr> elements, and that is what makes mobile one rule
The card is a table. The header is a row, the footer is a row, and whatever the calling module supplies is one or more rows in between. That constraint is what lets the entire phone layout be a single selector:
@media only screen and (max-width:600px) {
.cp-outer { padding: 20px 8px !important; }
.cp-card > tbody > tr > td { padding-left: 20px !important; padding-right: 20px !important; }
}
Every row of the card is a direct <td> of .cp-card, so one rule pulls the 32px side padding in to 20px for all of them, and no individual email needs its own classes or its own mobile variant.
The > tbody > tr > part is not decoration. Emails are tables inside tables: a score card, a quote block, a button. Writing .cp-card td would reach into all of them and flatten padding that was chosen deliberately at a nested level. Scoping to direct children means the rule applies to the card's own rows and stops there. And tbody has to be written out because browsers and rendering engines insert it whether your markup has it or not, so a .cp-card > tr selector matches nothing.
Two footnotes:
!important is not sloppiness here. Every other style in the document is an inline style attribute, because that is what survives CSS stripping. An inline style beats a stylesheet rule on specificity, so without !important the media query loses every time. In email this is the normal state of affairs, not a smell.
Outlook desktop ignores media queries entirely. That is fine, and worth stating out loud rather than fighting: Outlook desktop is never 600px wide. The media query is for phones, and the phone clients that matter, Apple Mail and the Gmail app, do support it.
The card itself carries both a fixed width and a fluid one:
<table width="560" cellpadding="0" cellspacing="0" role="presentation" class="cp-card"
style="background:#ffffff;border-radius:10px;max-width:560px;width:100%">
width="560" as an attribute for the engines that ignore CSS widths, width:100% with max-width for the ones that do not. Belt and braces, and they do not conflict because the attribute is only consulted when the CSS is not.
preheader is the only copy you did not write
The preheader is the grey line the inbox shows next to the subject. If you do not set it, the client makes one up, and what it picks is the first text in your document, which is usually "View in browser" or the alt text of a logo.
const preheaderBlock = preheader
? `\n <div style="display:none;max-height:0;overflow:hidden;opacity:0;mso-hide:all">${preheader}${'​ '.repeat(60)}</div>`
: '';
Four ways of hiding it, because no one of them works everywhere: display:none is the obvious one, max-height:0 plus overflow:hidden catches clients that ignore it, opacity:0 catches the rest, and mso-hide:all is the Word engine's own directive.
The repeated ​ at the end is the trick worth knowing. A zero width space followed by a non-breaking space, sixty times over, pads the preview text out so the client stops pulling in the first sentence of your body copy after your preheader ends. Without it, you write a tight one line preview and the inbox shows it followed by "Hi there, we noticed you" trailing off.
Note the block carries its own leading newline and indent, so omitting the preheader leaves no whitespace-only line after <body>. Trivial, but generated HTML that is also readable HTML is worth the two characters.
unsubscribeUrl is nullable, and both branches are real
export function emailFooter(
unsubscribeUrl: string | null,
reason: string = DEFAULT_FOOTER_REASON
): string
Most systems treat the unsubscribe link as unconditional, which forces every sender to have one, which means the one-off email you send to a person who is not a user yet has to invent a token for somebody who does not exist. Making it nullable drops the marketing lines entirely for those, and keeps the legal ones.
When it is present, the URL is login-less by necessity, so it is an HMAC:
token = HMAC-SHA256(userId, UNSUBSCRIBE_SECRET).hex
The email side generates it and the route side verifies it, and those are two files that must agree forever. The test that guards it does not import the route: it reimplements the verification independently and asserts the generated token passes, with a timingSafeEqual comparison. If the schemes ever diverge, the failure is a red test rather than every unsubscribe link in the wild silently breaking, which is not a thing you discover from your own inbox.
The footer also carries a "why am I getting this" line, overridable per email:
export const DEFAULT_FOOTER_REASON =
"You're receiving this because you submitted an interview practice session.";
Spam complaints are mostly a memory problem. The person forgot they signed up. One sentence naming the action they took is the cheapest deliverability work available.
See it: the footer's two links are cogniprep.app/privacy and, when you click the unsubscribe, cogniprep.app/unsubscribed. Both are real pages you can open now; the second one is deliberately a full page with a way back rather than a bare confirmation string, because someone who unsubscribes by accident should be one click from fixing it.
One thing that looks like a mistake and is not
// Hardcoded rather than read from NEXT_PUBLIC_WEBSITE_URL: these are the emailed
// shell's header/footer links (logo, privacy), which must point at the real site
// even when sent from a machine whose env points at localhost for local dev.
const SITE_URL = 'https://cogniprep.app';
Everywhere else in the codebase, a URL comes from the environment. Not here. An email is the one artefact that outlives the process that produced it: send a test from a dev machine and a http://localhost:3000/logo-light.png header is a broken image forever, in someone else's inbox, with no way to fix it after the fact. The env var is the right default for a web page and the wrong one for a message you cannot recall.
If you want to see the shell itself, sign up at cogniprep.app and look at the first email on a phone. The 20px gutters, the preview line, and the footer reason are all from the eighty lines above.
Top comments (0)