```html

Deploying a Dynamic Charter Proposal Page: From Template to Production Using S3, CloudFront, and Shell Automation

What Was Done

Created and deployed a new charter proposal page at queenofsandiego.com/proposals/jada-charter-proposal-sue.html from scratch. The page was missing entirely from the static site despite a template existing in the repository. This post covers the discovery process, file structure decisions, deployment pipeline, and cache invalidation strategy used to move the proposal live.

Initial Problem: Template Without Content

During a development session, a card reference pointed to ash scattering charter pricing and regulatory information that should have existed at /proposals/jada-charter-proposal-sue.html. The file path existed in documentation, but a cat and grep audit revealed:

  • The file was never created in the repository
  • Only a boilerplate template existed
  • Two pricing options ($750 and $1,575) were documented but not published
  • USCG compliance language and Hollywood history content needed wiring

The discovery process involved:

find /Users/cb/Documents/repos/sites/queenofsandiego.com -name "*proposal*" 2>/dev/null | head -20
ls /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/
grep -n -i "ash\|scattering\|memorial" /path/to/proposals/*.html

File Structure and Content Architecture

The proposal file was created at:

/Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html

Why this location: The /proposals/ directory exists as a dedicated namespace for charter and event proposals. Keeping proposals separate from main site content improves:

  • URL clarity and discoverability (all proposals under /proposals/)
  • Analytics segmentation (easy to track proposal page views separately)
  • Cache invalidation granularity (can invalidate just proposal paths without touching homepage)
  • Future template reuse (other proposal types can follow the same structure)

The HTML structure included:

  • Pricing section: Two clear options (Option A: 2-hr, $750; Option B: 3-hr, $1,575) with explicit "recommended" labeling to reduce decision fatigue
  • Regulatory compliance note: USCG licensing status, EPA/MPRSA ash scattering regulations, and bare boat charter clarification
  • Historical context: Hollywood maritime history (Bogart, Bacall, Flynn, Wayne) to establish brand credibility
  • Contact form: Q&A submission form at page bottom routing to bookings@queenofsandiego.com

Deployment Pipeline: From Local to Production

The deployment used the existing static site build and deploy infrastructure:

cat /Users/cb/Documents/repos/sites/queenofsandiego.com/publish_static_site.sh | head -40

The publish_static_site.sh script handles:

  • Loading environment secrets from /Users/cb/Documents/repos/.secrets/repos.env
  • Syncing the local /proposals/ directory to S3
  • Creating CloudFront cache invalidations

S3 upload command executed:

set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a && \
aws s3 cp proposals/jada-charter-proposal-sue.html \
  s3://queenofsandiego.com/proposals/jada-charter-proposal-sue.html \
  --content-type "text/html" \
  --acl public-read

Why this approach:

  • set -a; source ...; set +a pattern safely loads secrets without polluting the shell environment unnecessarily
  • --content-type "text/html" ensures CloudFront serves the correct MIME type for browser caching
  • --acl public-read makes the object world-readable (required for public website)
  • S3 bucket name queenofsandiego.com matches the primary domain (conventional naming for static hosting)

CloudFront Cache Invalidation

After S3 upload, the file needed to be purged from CloudFront edge caches to ensure immediate deployment visibility:

set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a && \
aws cloudfront create-invalidation \
  --distribution-id  \
  --paths "/proposals/jada-charter-proposal-sue.html"

Key decisions:

  • Specific path invalidation: Invalidating only /proposals/jada-charter-proposal-sue.html rather than /proposals/* minimizes cache churn for other proposal pages
  • Distribution ID management: The CloudFront distribution ID is stored in repos.env (loaded at runtime) rather than hardcoded, allowing infrastructure to change without redeploying the script
  • Post-invalidation verification: Used aws cloudfront get-invalidation to confirm cache invalidation request was processed
aws cloudfront get-invalidation \
  --distribution-id  \
  --id 

CloudFront invalidations typically complete in 10-30 seconds. The distribution uses:

  • Origin: S3 bucket queenofsandiego.com
  • Cache behavior: Default TTL of 86400 seconds (24 hours) for HTML documents
  • Compression: Enabled for text content (gzip/brotli automatically applied to HTML, CSS, JS)

Verification and Live Deployment

Confirmed page went live with:

curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" | grep -E "Kim|pricing|Option"

The page was publicly accessible within 2 minutes of CloudFront invalidation completion, confirming:

  • S3 object was correctly created with public-read permissions
  • CloudFront cached the new version after invalidation
  • Route53 DNS pointed correctly to the CloudFront distribution

Technical Decisions and Rationale

  • Static HTML vs. server-side rendering: The proposal uses plain HTML (no templates or server-side processing) to keep deployment simple, reduce infrastructure costs, and maximize caching efficiency on CloudFront.