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 +apattern 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-readmakes the object world-readable (required for public website)- S3 bucket name
queenofsandiego.commatches 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.htmlrather 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-invalidationto 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.