Hardening Email Rendering Across Gmail Web & Mobile: A Case Study in CSS Containment & CloudFront URL Rewriting

This post documents a production incident and the infrastructure fixes deployed to prevent email rendering failures in Gmail Web, plus a parallel effort to eliminate 404s across a legacy domain using CloudFront Functions.

The Problem: Gmail Web Stripping Outer `<div>` Backgrounds

On May 17, a crew dispatch email sent via AWS SES rendered as unreadable white text on white background when viewed in Gmail Web. The root cause: the email template wrapped the entire message in a `<div style="background:#0a1628">`, expecting the dark navy background to render as the email backdrop.

Gmail Web strips outer `<div>` styles at the message boundary. Mobile clients and Outlook preserve them, but the Web client—where most users read mail—rendered the wrapper as transparent, leaving cream-colored text (`#f3efe7`) on the default white canvas. This broke readability for 14 crew members and required immediate resend.

Technical Solution: Table-Wrapped Backgrounds with `!important` & CSS Containment

The fix replaces the unsafe `<div>` wrapper with an explicit `<table>` structure, applying `bgcolor` attributes at every cell level with `!important` overrides:

<table bgcolor="#0a1628" cellpadding="0" cellspacing="0" 
       style="background-color: #0a1628 !important; width: 100%;">
  <tr>
    <td bgcolor="#0a1628" style="background-color: #0a1628 !important; padding: 20px;">
      <!-- content -->
    </td>
  </tr>
</table>

Why this works:

  • HTML `bgcolor` attributes: Predate CSS and are respected by all email clients, including Gmail Web's sanitizer, because they're not style-strippable.
  • `!important` on every `style`: Defeats Gmail's specificity resets without triggering spam filters (used sparingly, only on color safety-critical rules).
  • Per-cell containment: Each `<td>` carries its own background declaration, preventing inheritance collapse if outer layers are stripped.

The corrected email was resent May 20 via `/tmp/uniform_resend.py` (custom SES wrapper) to the original 14-person crew list, with an added validation gate: no email can be sent until it renders correctly in a Gmail Web pane simulator.

Infrastructure: Email Build & Delivery Pipeline

The resend flow involved three layers:

1. Template Layer — Updated `/var/folders/_h/.../memory/feedback_email_light_theme.md` to document the "blue background, gold accents, white text" JADA brand standard and the new `<table bgcolor>` rule.

2. Build Layer — Created `/tmp/uniform_resend.py`, a Python script that:

  • Reads the crew roster from DynamoDB (queenofsandiego crew table)
  • Constructs the email with the hardened table-based HTML
  • Invokes AWS SES `SendEmail` API with UTF-8 charset and explicit `MessageId` tracking
  • Logs delivery status (resulting MessageId: `0100019e45b138ad-…`)

3. Delivery Layer — SES was chosen over SNS/Lambda for batch sends because it provides direct MessageId tracking and doesn't trigger the per-message Lambda cold start overhead. Travis Neel, marked as SMS-only in the crew system, received the rule via Twilio SMS to 530-262-5427 instead.

New Page: Uniforms Reference at `/uniforms.html`

To ensure the crew had a persistent, readable reference, a new page was deployed:

  • Path: `/Users/cb/Documents/repos/sites/queenofsandiego.com/uniforms.html`
  • Live URL: https://queenofsandiego.com/uniforms.html
  • Design: Dark navy (`#0a1628`) with gold accent borders, matching the resend email and JADA brand
  • Content: Two cards — one for ash scattering uniform requirements, one for standard charters
  • Meta tag: `<meta name="robots" content="noindex">` — internal reference only, not SEO-indexed
  • Deployment: Pushed to S3 bucket `queenofsandiego-prod` with CloudFront invalidation (no overwrite, new file)

Parallel Fix: CloudFront Function for 404 Elimination on dangerouscentaur.com

While addressing email rendering, a separate incident surfaced: dangerouscentaur.com was returning 404s for valid URLs. Root cause: the site had been migrated but the routing rules hadn't been updated.

CloudFront Function Deployment

Created `/tmp/dc-clean-urls.js`, a CloudFront Function (viewer request trigger) that rewrites incoming paths:

// Pseudocode structure
if (request.uri.endsWith('.html')) {
  request.uri = request.uri.replace(/\.html$/, '');
}
if (!request.uri.includes('.')) {
  request.uri += '/index.html';
}
return request;

Deployment Steps:

  • Created function in CloudFront console, associated with distribution ID (dangerouscentaur)
  • Set stage to LIVE (not DEVELOPMENT)
  • Attached to viewer-request event for the origin distribution
  • Tested with sample paths: `/about` → `/about/index.html`, `/contact.html` → `/contact`
  • Live verification confirmed 200 responses instead of 404

Why CloudFront Functions, not Lambda@Edge?

  • Sub-millisecond latency (JavaScript runs at edge, no external invoke)
  • No cold starts or IAM role overhead
  • Synchronous, inline URL rewriting before origin request
  • Cost: ~$0.60/million requests vs. Lambda@Edge at $0.60 per million + compute time

Uptime Monitoring: Lambda + DynamoDB

To prevent future dangerouscentaur outages from going undetected, deployed an uptime Lambda:

  • Function name: `dc-uptime-check` (created via `/tmp/dc_uptime_lambda.py`)
  • Trigger: CloudWatch Events, every 1 minute
  • Logic: HTTP GET to dangerouscentaur.com/health, record result (200 vs. non-200) in DynamoDB table `dc-uptime-logs`
  • IAM role: `dc-uptime-lambda-role` with policies for DynamoDB write + CloudWatch logs