Hardening Email Rendering in SES: A Deep Dive into Gmail Web Compatibility and CloudFront URL Rewriting
This post covers two parallel infrastructure challenges resolved in a single development session: fixing HTML email rendering failures in Gmail Web and implementing a zero-downtime URL rewriting layer for a legacy static site.
The Email Rendering Crisis: Why Gmail Strips Your Styling
A crew notification email sent via AWS SES rendered as unreadable cream text on white background in Gmail Web, causing the entire crew roster to ignore the message. Root cause analysis revealed a subtle but critical CSS cascade issue:
- The Problem: The email template wrapped content in a
<div style="background:#0a1628">container. Gmail Web's sandbox strips outer<div>background styles as a security measure, leaving content text (#f3efe7 cream) against the browser's default white background—unreadable. - Why This Matters: Email client rendering is fragmented. Desktop Gmail Web, Gmail mobile, Outlook, and Apple Mail each have different CSS parsing rules. A design that works in preview (often rendered with Chromium) fails in production.
- The Fix: Replaced the outer
<div>wrapper with a<table bgcolor="#0a1628">structure, with every<td>explicitly carryingbgcolor="#0a1628" style="background:#0a1628 !important". This dual-declaration approach ensures compatibility across clients that parse inline styles differently.
Implementation: SES-Hardened Email Architecture
The corrected email template was deployed through a new validation gate before any SES blast:
# File: /tmp/uniform_resend.py
# Process:
# 1. Load base email template from memory (JADA standard: dark navy #0a1628, gold accents)
# 2. Wrap content in table-based structure, not div
# 3. Apply bgcolor to every td element
# 4. Add color-scheme: dark meta tag for clients that respect it
# 5. Include explicit text-align and font-family on each cell
# 6. Send via SES with MessageId tracking
# 7. Log to managercandy dashboard for audit trail
Key changes encoded into team memory:
- File:
/Users/cb/.claude/projects/-Users-cb-Documents-repos/memory/feedback_email_light_theme.md— documents why dark backgrounds require table-based rendering - File:
/Users/cb/.claude/projects/-Users-cb-Documents-repos/memory/MEMORY.md— updated with "SES dark theme rule: never use div wrappers, always table bgcolor + !important + color-scheme meta" - New validation: all email blasts must pass Gmail Web pane readability check before dispatch
The resend was successful: 14 crew members received the corrected email (SES MessageId starting with 0100019e45b138ad), with a link to a newly deployed uniforms information page at https://queenofsandiego.com/uniforms.html.
Secondary Infrastructure: CloudFront URL Rewriting for Legacy Static Sites
In parallel, dangerouscentaur.com began returning 404 errors due to missing URL routing logic. The site is a static S3-hosted asset but requires intelligent path rewriting—a common pattern with legacy single-page apps or poorly migrated sites.
Problem: Requests to paths like /path/to/page returned 404 because S3 has no routing rules configured, and the site's hero image parallax CSS was referencing assets via outdated root paths.
Solution: Implemented a CloudFront Function (not Lambda@Edge, due to latency requirements) to rewrite incoming requests and provide fallback routing:
- Resource Created: CloudFront Function named
dc-sites-router - Deployment: Published to LIVE (not staging) and attached to the dangerouscentaur.com CloudFront distribution (Distribution ID visible in CloudFront console)
- Function Logic: Intercepts viewer requests, checks for file extensions, and rewrites directory paths to index.html or a fallback route
- File Location:
/tmp/dc-clean-urls.js(source code for reference; actual function deployed via CloudFront console)
# Test command to verify CF function behavior:
curl -I https://dangerouscentaur.com/some/nested/path
# Expected: 200 OK (rewritten to valid asset or index)
# Before: 404 Not Found
Uptime Monitoring: Lambda + DynamoDB for Site Health
To prevent future 404 cascades, a proactive monitoring layer was added:
- DynamoDB Table: Created with on-demand billing to log uptime check results. Partition key:
domain, sort key:timestamp - IAM Role: Created with minimal permissions: CloudWatch Logs (PutLogEvents) and DynamoDB (PutItem)
- Lambda Function:
dc_uptime_lambda.py— runs every 1 minute via EventBridge rule, performs HEAD requests to key dangerouscentaur.com paths, logs results - Tracking: Results logged to managercandy dashboard for visibility; alarms can be configured on DynamoDB scan patterns
This pattern is reusable: any static site with routing complexity now has a template to follow.
Why These Decisions Matter
Email Rendering Fragmentation: Email clients are more restrictive than browsers. The CSS Cascade and box model work differently. Testing in a single preview tool is insufficient; always validate against Gmail Web, Outlook, and Apple Mail rendering engines.
CloudFront Functions vs. Lambda@Edge: CloudFront Functions execute at edge locations with <100ms latency and no cold start. Lambda@Edge has higher latency (10–100ms) and higher cost. For URL rewriting, CloudFront Functions are the right choice.
S3 Static Hosting Limitations: S3 alone cannot rewrite paths intelligently. S3 website endpoints have no routing rules comparable to a web server. CloudFront Functions provide the missing layer without extra infrastructure.
Artifacts and Deployments
- Uniforms Page:
/Users/cb/Documents/repos/sites/queenofsandiego.com/uniforms.html— deployed to S3 bucketqueenofsandiego.com, markednoindex, styled with navy/gold brand colors - Email Template Standard: Codified in team memory with exact table structure and
!importantplacement requirements - CloudFront Function:
dc-sites-router— attached to dangerouscentaur distribution, live in production - Uptime Lambda: Scheduled via EventBridge, logs to DynamoDB and CloudWatch
- Audit Trail: All actions logged to managercandy dashboard (
progress.queenofsandiego.com)