Deploying a Dynamic Charter Proposal System: From Static Template to Live S3+CloudFront Architecture
Overview: The Problem
A charter proposal template existed in the codebase at /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html, but the actual rendered file was never deployed to the live S3 bucket. This meant requests to queenofsandiego.com/proposals/jada-charter-proposal-sue.html would fail or serve stale content. The task: author the proposal HTML, deploy it to S3, invalidate the CloudFront cache, and verify live delivery.
What Was Done
- Created/Modified:
/Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html— multi-pass HTML authoring with pricing tiers, legal compliance language, and embedded Q&A form - Deployed: Pushed to S3 bucket (sourced from
.secrets/repos.env) using AWS CLI - Cache Invalidation: Triggered CloudFront distribution invalidation to purge edge caches
- Verification: Live curl test against production domain confirmed delivery
Technical Architecture & Implementation Details
Static Site Generation & Deployment Pipeline
The Queen of San Diego website uses a Git-driven static site deployment model:
- Source: Local filesystem repository at
/Users/cb/Documents/repos/sites/queenofsandiego.com/ - Storage: AWS S3 bucket (name stored as environment variable in
.secrets/repos.env) - CDN: CloudFront distribution (ID also environment-sourced)
- Deploy Script:
publish_static_site.shorchestrates the upload via AWS CLI
This pattern avoids application servers entirely — all content is pre-rendered HTML served directly from S3 with CloudFront caching. This reduces operational complexity and latency.
Deployment Commands
The deployment workflow follows this sequence:
# 1. Source environment credentials (non-interactive)
set -a
source /Users/cb/Documents/repos/.secrets/repos.env
set +a
# 2. Upload proposal HTML to S3
aws s3 cp proposals/jada-charter-proposal-sue.html \
s3://${S3_BUCKET_NAME}/proposals/jada-charter-proposal-sue.html \
--content-type "text/html; charset=utf-8"
# 3. Invalidate CloudFront cache for this path
aws cloudfront create-invalidation \
--distribution-id ${CLOUDFRONT_DIST_ID} \
--paths "/proposals/jada-charter-proposal-sue.html"
# 4. Poll invalidation status
aws cloudfront get-invalidation \
--distribution-id ${CLOUDFRONT_DIST_ID} \
--id ${INVALIDATION_ID}
Why this approach? The set -a/set +a pattern exports all variables from the env file without exposing them in shell history. Using environment variables instead of hardcoded values enables multi-environment support (dev/staging/production) without code changes.
Content Type Handling
The --content-type flag is critical: it sets the Content-Type HTTP header that S3 serves. Without it, browsers might download the HTML as a file rather than rendering it. The charset specification ensures proper Unicode handling in the browser.
File Structure & Proposal Content
The HTML proposal includes three main sections:
- Pricing Options — Two tiers:
- Option A (recommended): 2-hour charter, Captain Sergio only — $750 flat
- Option B: 3-hour charter, full crew — $1,575 flat
- Legal Compliance Footer — References USCG licensing, EPA/MPRSA ash scattering regulations, and bareboat charter exclusions. This protects the business from liability and sets clear expectations.
- Historical Context — Hollywood Golden Age references (Bogart, Bacall, Flynn, Wayne) tied to San Diego maritime history, adding cultural value to the offering.
- Interactive Q&A Form — Bottom-of-page conversion funnel; submits to
bookings@queenofsandiego.com
Design Decision: Limited to exactly two pricing options. This reduces decision fatigue — a well-documented UX principle where too many choices paralyze purchasing decisions. Option A is explicitly marked "recommended" to guide prospects toward the higher-margin offering while respecting budget-conscious alternatives.
Infrastructure & DNS Resolution
The deployment chain assumes this infrastructure (all names/IDs stored securely in .secrets/repos.env):
Domain: queenofsandiego.com
↓ (DNS via Route53 or external registrar)
CloudFront Distribution (ID: env variable)
↓ (Origin)
S3 Bucket (Name: env variable)
└── /proposals/jada-charter-proposal-sue.html
Cache Invalidation Strategy: CloudFront invalidation is asynchronous. The create-invalidation call returns an invalidation ID; you must poll get-invalidation to confirm completion (typically 60–120 seconds). The verification step confirms this propagated:
curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" \
| grep -E "charter|proposal|price" \
| head -5
This grepping for known keywords confirms the live version contains the expected content.
Key Decisions & Rationale
- Static HTML over dynamic content: No database, no runtime — just pre-rendered files. Faster, cheaper, more resilient. Trade-off: changes require a re-deploy (acceptable for proposal documents).
- S3 + CloudFront over traditional web server: Eliminates EC2 costs, patching burden, and scaling complexity. CloudFront's global edge locations ensure sub-100ms latency worldwide.
- Environment variable injection: Keeps credentials out of version control. The
.secrets/repos.envfile is.gitignore-d and never committed. - Explicit content-type header: Prevents browser download/rendering issues; especially important for forms that need JavaScript interactivity.
- Bare-boat legal language: Protects the charter operator. EPA/MPRSA regulations govern ash scattering at sea; this disclosure shifts responsibility to the client to verify compliance.
What's Next
- Photo Integration: The proposal currently lacks hero imagery. Path forward: source high-resolution photos of the vessel and San Diego landmarks, optimize for web (WebP format, ~200KB max), and wire into the HTML via
<img src="assets/images/..."&