Deploying a Dynamic Charter Proposal System: From Template to Production S3 + CloudFront
Overview: The Problem
A charter proposal page existed only as a template—the actual HTML file was never generated and deployed to production. This meant that while the infrastructure was in place, the customer-facing proposal document at queenofsandiego.com/proposals/jada-charter-proposal-sue.html returned a 404. This post walks through the complete deployment workflow: file creation, S3 upload, CloudFront cache invalidation, and verification.
What Was Done
- Created production-ready HTML proposal file with pricing tiers, regulatory compliance notes, historical context, and booking form
- Uploaded file to S3 bucket
queenofsandiego.comin theproposals/prefix - Invalidated CloudFront distribution cache to force edge nodes to fetch updated content
- Verified live deployment and response headers
Technical Details: File Structure and Content Strategy
The proposal needed to balance marketing copy with clear decision-making. The final HTML document included:
- Pricing Options (Decision Tree): Exactly 2 options to minimize decision fatigue per card requirements. Option A (2-hour, captain only) at $750 and Option B (3-hour, full crew) at $1,575, with A marked as recommended.
- Regulatory Compliance Section: Clear statement that this is USCG-licensed charter service (not a bareboat charter) and complies with EPA/MPRSA regulations for ash scattering ceremonies.
- Historical Context: Hollywood Golden Age references (Bogart, Bacall, Flynn, Wayne) to establish heritage and emotional connection to San Diego maritime history.
- Call-to-Action: Q&A form at page footer collecting inquiry details, posting to
bookings@queenofsandiego.com(generic booking address, not personal email).
File path: /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html
Infrastructure: S3 + CloudFront Architecture
S3 Bucket Configuration
The S3 bucket queenofsandiego.com serves as the origin for the static website. The bucket is configured with:
- Static Website Hosting: Enabled on bucket, serving index documents from root and subdirectories
- Object Prefix Organization: Proposals live under
proposals/prefix to separate marketing collateral from primary site content - Access Control: CloudFront origin access identity (OAI) manages S3 read permissions; the bucket itself is not publicly accessible
Upload command executed:
aws s3 cp proposals/jada-charter-proposal-sue.html \
s3://queenofsandiego.com/proposals/jada-charter-proposal-sue.html \
--content-type "text/html; charset=utf-8"
The --content-type flag ensures browsers render the file as HTML rather than as a generic binary download.
CloudFront Distribution and Cache Invalidation
CloudFront sits in front of the S3 bucket, providing:
- Global edge caching: Content cached at 200+ edge locations worldwide
- HTTPS termination: All requests encrypted in transit
- HTTP/2 push: Optimized delivery for multi-file page loads
After uploading the file to S3, we triggered a cache invalidation on the CloudFront distribution. This tells edge nodes to discard cached copies and fetch fresh content from S3 on the next request.
aws cloudfront create-invalidation \
--distribution-id \
--paths "/proposals/jada-charter-proposal-sue.html"
Why invalidate? Without invalidation, edge nodes in different geographic regions would continue serving the old (nonexistent) version of the file for up to 24 hours, depending on the TTL (time-to-live) setting. Invalidation is the only way to guarantee immediate propagation.
Cost consideration: Each CloudFront invalidation request incurs a small charge after the first 3,000 paths per month. For a single file, this is negligible, but at scale, batch invalidations become important.
Deployment Verification
After deploying, we verified the live endpoint:
curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" \
-I
Expected response headers:
HTTP/2 200 OK(successful retrieval)Content-Type: text/html; charset=utf-8(correct MIME type)X-Cache: Hit from cloudfront(confirms edge delivery after invalidation)Age: 0(fresh copy from origin immediately post-invalidation, increases as edge TTL expires)
Key Architectural Decisions
Why Static HTML Over Dynamic Generation?
The proposal is a semi-static document with occasional updates (pricing, regulatory language, contact email). Generating it from a database on every request would add latency and cost with minimal benefit. Static HTML + git version control + manual redeploy on changes provides:
- Sub-100ms response times from global edge cache
- Zero database queries
- Easy audit trail via git history
- Trivial to revert to previous versions
S3 + CloudFront vs. Other Options
Alternatives considered and rejected:
- Lambda@Edge: Adds complexity for content that doesn't require dynamic transformation
- Application Load Balancer + EC2: Higher cost and operational overhead for static files
- GitHub Pages: Separates proposal content from the main queenofsandiego.com domain; complicates DNS and branding
Contact Email Strategy
The form originally referenced Carole's personal email but was updated to use the generic bookings@queenofsandiego.com address. This:
- Decouples the proposal from a single person's inbox (Carole can leave, retire, or go on vacation without breaking the booking flow)
- Allows multiple team members to monitor and respond to inquiries
- Provides a professional, business-standard contact point rather than a personal email
What's Next
- Photo integration: Pending receipt of high-resolution vessel and interior photos to be embedded in the proposal; will update HTML and re-deploy
- A/B testing: CloudFront can serve variant proposals based on query parameters or headers to test pricing