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:
- Generate/edit HTML locally
- Run
aws s3 cpto upload the file to the bucket - 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