Hardening Email Rendering in SES: From Broken Dark Themes to Table-Based Layouts
A crew-wide email blast for ash scattering uniform requirements arrived unreadable—cream text on white background—because Gmail Web's DOM sanitization stripped the outer <div> background color. This post covers the technical fix, infrastructure changes, and architectural patterns we implemented to prevent future rendering failures in transactional email.
The Problem: CSS Cascade Failure in Gmail Web
On May 17, a uniform requirement email was sent via Amazon SES using an HTML template that wrapped the entire message in:
<div style="background:#0a1628; color:#f3efe7;">
<!-- content -->
</div>
When Gmail Web rendered this, it stripped the outer <div> wrapper as a sanitization measure. The result: cream-colored text (#f3efe7) landed on Gmail's default white background, rendering the email completely illegible. Fourteen crew members received an email they couldn't read.
Root cause: Gmail Web sanitizes arbitrary <div> styling to prevent phishing and malicious overlays. We relied on an unsupported pattern.
Technical Solution: Table-Based Layout with Inline Style Hardening
The fix involved restructuring the SES email template to use table-based layouts with explicit inline styles on every cell:
<table width="100%" cellpadding="0" cellspacing="0" bgcolor="#0a1628" style="background-color:#0a1628 !important;">
<tr>
<td bgcolor="#0a1628" style="background-color:#0a1628 !important; color:#ffffff; padding:20px;">
<h1 style="color:#d4af37 !important;">Uniform Requirements</h1>
<!-- content -->
</td>
</tr>
</table>
Why this approach:
- Table-based layouts predate CSS and remain the most reliable rendering method across email clients. Gmail Web preserves table structure and cell attributes.
- Dual-layer background specification: Both the deprecated
bgcolorattribute and inlinestyleensure fallback coverage across client variants. !importantflags override Gmail's own user-level theme preferences that might invert colors in dark mode.- Per-cell styling: Nested
<td>elements each carry explicit background and text color, preventing inheritance collapse.
Infrastructure Changes
New Files Created
- /Users/cb/Documents/repos/sites/queenofsandiego.com/uniforms.html — New landing page documenting uniform standards. Deployed to production S3 bucket
queenofsandiego-prod-weband distributed via CloudFront (Distribution ID:E2KZXX...). Markednoindexto prevent search indexing of internal documentation. - /tmp/uniform_resend.py — Python 3 script using boto3 SES client to resend corrected email with hardened template. Script includes a pre-send Gmail readability gate that validates contrast ratios before dispatch.
Deployment Process
The uniforms page was deployed in two stages:
# Stage 1: Staging environment validation
aws s3 cp uniforms.html s3://queenofsandiego-staging-web/uniforms.html \
--profile engineering \
--metadata "version=1,author=cb,timestamp=$(date +%s)"
# Invalidate staging CloudFront (Distribution: E3KZAA...)
aws cloudfront create-invalidation \
--distribution-id E3KZAA... \
--paths "/uniforms.html" \
--profile engineering
# Stage 2: Production deployment
aws s3 cp uniforms.html s3://queenofsandiego-prod-web/uniforms.html \
--profile engineering \
--metadata "version=1,author=cb,timestamp=$(date +%s)"
# Invalidate production CloudFront
aws cloudfront create-invalidation \
--distribution-id E2KZXX... \
--paths "/uniforms.html" \
--profile engineering
Email Delivery and Tracking
SES Send Details:
- Recipient count: 14 crew members
- SES Message ID:
0100019e45b138ad-... - Configuration Set:
queenofsandiego-transactional(enables delivery and bounce tracking) - Template source: Hardened dark-theme template stored in
/Users/cb/.claude/projects/-Users-cb-Documents-repos/memory/feedback_email_light_theme.md
Supplemental contact via SMS: Travis Neel (crew manager who requested no email communication) received an SMS to 530-262-5427 with the uniform rule and direct link to the uniforms page.
Blast Tracking and Documentation
To prevent regression and enable post-mortems, the blast was logged on two internal dashboards:
- Manager Candy dashboard: Logged with AWS profile
engineeringusing the endpoint pattern:POST /api/blastswith metadata (template version, recipient count, SES message ID) - Kanban board: Issue
card-t-1faa1eb1updated athttps://progress.queenofsandiego.com/with root cause analysis and fix verification
Memory and Pattern Codification
To ensure this failure mode doesn't recur, we documented the anti-pattern and fix in markdown memory:
New rule added to team memory: "All SES emails must use table-based layouts with explicit bgcolor attributes on every <td>. Outer <div> wrappers are banned. All color specifications must include !important flags. A Gmail readability gate (contrast ratio validation) is required before any crew-wide blast is sent."
Key Design Decisions
- Why not CSS frameworks like MJML? MJML would abstract away the table structure and potentially reintroduce the
<div>wrapping problem. We chose explicit table markup for maximum control. - Why mark the uniforms page as noindex? The page contains internal crew documentation that shouldn't appear in public search results. It's discoverable only via direct link and internal navigation.
- Why SMS for Travis instead of email? He explicitly requested no email communication. SMS is the appropriate channel for time-sensitive crew coordination.
- Why deploy to staging first? Allows validation of S3 bucket permissions, CloudFront cache invalidation, and Route53 DNS behavior before production release.