```html

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.com in the proposals/ 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