Deploying a Dynamic Proposal Page to S3 + CloudFront: Ash Scattering Charter Pricing & Legal Compliance

Overview

This post documents the end-to-end deployment of a proposal page (jada-charter-proposal-sue.html) to production infrastructure. The challenge: a template existed but the actual proposal file was never created or deployed, leaving the public URL returning a 404. We solved this by creating a fully-featured HTML proposal with pricing tiers, legal compliance notices, and historical context—then deployed it through S3 + CloudFront invalidation.

What Was Built

A single-page proposal document at queenofsandiego.com/proposals/jada-charter-proposal-sue.html containing:

  • Two pricing options with decision-fatigue reduction (Option A: 2-hr Captain-only charter at $750; Option B: 3-hr full crew at $1,575)
  • Legal compliance section explicitly referencing USCG licensing requirements and EPA/MPRSA ash scattering regulations
  • Historical context (Humphrey Bogart, Lauren Bacall, Errol Flynn, John Wayne references)
  • Q&A form for prospect follow-up

Technical Architecture

File Structure

/Users/cb/Documents/repos/sites/queenofsandiego.com/
├── proposals/
│   └── jada-charter-proposal-sue.html    [NEW - created in this session]
├── assets/
│   └── images/
│       └── interior/                      [for proposal imagery]
├── publish_static_site.sh                 [deployment orchestration]
└── [other static pages]

The proposal file was created in the local repository at /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html, then synced to S3.

S3 Bucket Configuration

The site is hosted on S3 bucket queenofsandiego.com with static website hosting enabled. The deployment flow:

  1. Generate/edit HTML locally
  2. Run aws s3 cp to upload the file to the bucket
  3. Trigger CloudFront cache invalidation to serve fresh content immediately

Deployment Pipeline

Step 1: File Creation & Local Validation

The proposal HTML was authored locally with semantic markup, embedded CSS (no external stylesheets to reduce dependencies), and inline Q&A form handling. Multiple rounds of edits refined:

  • Pricing clarity and emphasis (Option A marked as "recommended")
  • Legal language (added USCG/EPA/MPRSA regulatory references)
  • Removal of personal contact details (replaced `carole@...` with generic `bookings@queenofsandiego.com`)
  • Tone alignment with brand voice (historical Hollywood references)

Step 2: S3 Upload

aws s3 cp proposals/jada-charter-proposal-sue.html \
  s3://queenofsandiego.com/proposals/jada-charter-proposal-sue.html \
  --acl public-read \
  --content-type text/html

Why this command: The --acl public-read flag ensures the object is readable by CloudFront and public browsers. The explicit --content-type prevents S3 from guessing (which can result in binary downloads instead of browser rendering). The file is uploaded to the proposals/ prefix matching the URL path.

Step 3: CloudFront Invalidation

aws cloudfront create-invalidation \
  --distribution-id [DISTRIBUTION_ID] \
  --paths "/proposals/jada-charter-proposal-sue.html" "/proposals/*"

Why this step: CloudFront caches all objects at edge locations globally. Without invalidation, users would see stale content for up to 24 hours (default TTL). By explicitly invalidating the path (and the wildcard /proposals/* for safety), we force edge nodes to fetch the latest version from the S3 origin on the next request. This is a synchronous operation; verification:

aws cloudfront get-invalidation \
  --distribution-id [DISTRIBUTION_ID] \
  --id [INVALIDATION_ID]

The status transitions from InProgress to Completed (typically within 60 seconds).

Step 4: Live Verification

curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" | head -50

Confirmed the page returns HTTP 200 and contains expected content markers (pricing, legal text, form elements).

Key Architectural Decisions

1. S3 + CloudFront vs. Dynamic Backend

Decision: Static HTML on S3 behind CloudFront.

Rationale: This is a proposal document, not a real-time transactional page. Static hosting offers:

  • 99.99% availability SLA (S3 replication, CloudFront global edge network)
  • Zero server maintenance
  • Per-request cost ~$0.001 (S3) + edge caching (CloudFront)
  • Instant geographic distribution (edge caches in 200+ cities)

2. Single HTML File vs. Template Partials

Decision: Self-contained HTML file (no external CSS/JS includes).

Rationale: Simplifies deployment (one file = one S3 object). Reduces external dependencies and cold-start latency. For a proposal document, this is appropriate; if the site were dynamic (e.g., form submissions to Lambda), we'd use a build pipeline.

3. Legal & Compliance Language Placement

Decision: Integrated into body copy rather than separate "Terms" section.

Rationale: Reduces decision fatigue and cognitive load. Prospect reads pricing → sees legal confidence signal → proceeds. Separate legalese typically gets skipped.

4. Email Abstraction

Decision: Removed personal email; used `bookings@queenofsandiego.com`.

Rationale: Separates operator identity (Carole) from business identity. Allows email routing/delegation changes without content updates.

Infrastructure Details

  • S3 Bucket: queenofsandiego.com (regional, us-west-2)
  • CloudFront Distribution: [DISTRIBUTION_ID] (global, ~200 edge locations)
  • DNS: Route53 alias record pointing CloudFront distribution (no origin server latency)
  • Caching Strategy: Default 24-hour TTL for HTML; invalidation override as needed