Deploying a Static Charter Proposal Page: S3 + CloudFront Cache Invalidation Pattern
What Was Done
Created and deployed a new static HTML proposal page for a yacht charter service at queenofsandiego.com/proposals/jada-charter-proposal-sue.html. The file was built from an existing proposal template, populated with charter pricing, legal compliance language, and historical content, then deployed to S3 with CloudFront cache invalidation to ensure immediate availability.
The Problem: Template Without Implementation
The session history shows repeated edits to a proposal file that existed only in the repository—never deployed to production. Searches for deployed proposals revealed the file was missing from the live S3 bucket at s3://queenofsandiego.com/proposals/. The template existed locally, but the actual page serving to end users did not.
Technical Details: File Structure and Content
The proposal was built as a single-file HTML document with embedded CSS and minimal JavaScript, following the static site pattern used throughout the queenofsandiego.com domain:
- File path (repository):
/Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html - Deployed location:
s3://queenofsandiego.com/proposals/jada-charter-proposal-sue.html - Public URL:
https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html
Content structure included:
- Charter pricing with two options: Option A ($750 flat, 2-hour, Captain Sergio only) and Option B ($1,575 flat, 3-hour, full crew)
- USCG and EPA/MPRSA compliance note explaining the vessel is licensed and not a bareboat charter—important for ash-scattering service legality
- Hollywood history section (Humphrey Bogart, Bacall, Flynn, Wayne era references)
- Interactive Q&A form at page footer for booking inquiry
Deployment Process: S3 + CloudFront Pipeline
The deployment followed the site's established static publishing workflow:
# Source environment variables (AWS credentials stored securely)
set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a
# Copy file to S3 bucket
aws s3 cp proposals/jada-charter-proposal-sue.html \
s3://queenofsandiego.com/proposals/jada-charter-proposal-sue.html \
--acl public-read \
--content-type "text/html; charset=utf-8"
# Invalidate CloudFront cache to serve updated content immediately
aws cloudfront create-invalidation \
--distribution-id [DISTRIBUTION_ID] \
--paths "/proposals/jada-charter-proposal-sue.html"
Why this two-step pattern? S3 is the origin storage layer, but CloudFront sits in front as a CDN cache. Without the invalidation step, edge nodes would serve the old (or nonexistent) version for up to 24 hours. The invalidation clears the cache, forcing CloudFront to refetch from S3 on the next request.
Infrastructure: S3 Bucket Configuration
The bucket queenofsandiego.com is configured as a static website hosting origin:
- Bucket name:
queenofsandiego.com - Region: Standard S3 region (likely us-west-1 or us-east-1)
- Public access: Enabled via bucket policy; files marked with
--acl public-readare readable by anonymous HTTP requests - Content type: Explicitly set to
text/html; charset=utf-8to ensure browsers parse HTML correctly
Verification of deployment used AWS S3 API:
aws s3api head-object \
--bucket queenofsandiego.com \
--key "proposals/jada-charter-proposal-sue.html"
This confirms the object exists and returns metadata (size, etag, last-modified timestamp).
CDN Layer: CloudFront Distribution
CloudFront serves as the CDN edge network:
- Origin: S3 bucket
queenofsandiego.com - Distribution type: Web distribution (HTTP/HTTPS)
- Cache behavior: Default TTL 24 hours for HTML files; custom headers may vary by path pattern
- HTTPS: Enabled (likely via ACM certificate for *.queenofsandiego.com)
Cache invalidation was monitored using:
aws cloudfront get-invalidation \
--distribution-id [DISTRIBUTION_ID] \
--id [INVALIDATION_ID]
This returns status: Completed when the cache purge finishes (typically 30–60 seconds).
Key Decisions and Design Rationale
Single HTML File vs. Multi-Component Architecture
The proposal is a single .html file with embedded CSS and JavaScript rather than a component-based system. This decision aligns with the site's pattern for proposal pages:
- Pros: No build step, no bundler, zero JavaScript framework overhead, instant cache busting (one file to invalidate), trivial to version-control and audit
- Cons: CSS duplication across files, limited reusability, larger per-file size if assets are embedded
- Trade-off justification: Proposal pages are low-traffic, rarely change, and don't benefit from code-splitting or lazy loading. Single-file simplicity outweighs modularity gains.
Why CloudFront Invalidation, Not Cache Headers?
Cache headers (Cache-Control, Expires) were not used here because:
- Proposal content is time-sensitive (pricing, legal language, contact info can change)
- A proposal URL is often shared via email or printed—users expect to see the latest version immediately
- Invalidation cost is negligible: AWS CloudFront includes 3,000 invalidation requests/month free
Metadata: No Meta Tags in Proposal Files
The template avoids duplicating site metadata (Open Graph tags, canonical URLs) in proposal files. This is intentional—proposals are internal documents, not SEO-driven landing pages. A user who receives queenofsandiego.com/proposals/jada-charter-proposal-sue.html should see pricing and booking info, not rich social preview metadata.
Testing and Verification
Post-deployment verification used curl to confirm the page was live and serving correct content:
curl -s "https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html" \
| grep -E "pricing|Option