Deploying a Dynamic Proposal Page: From Missing File to CloudFront CDN
What Was Done
A charter proposal page for the Queen of San Diego website was created from scratch and deployed to production. The file /Users/cb/Documents/repos/sites/queenofsandiego.com/proposals/jada-charter-proposal-sue.html existed only as a template reference but was never actually generated or published. This post documents the full lifecycle: file creation, S3 upload, CloudFront cache invalidation, and verification.
The Problem: Missing Production File
During development, a grep search across the proposals directory revealed the file was referenced in templates but never instantiated. The template system had a pattern matching jada-charter-proposal-*, but the actual HTML artifact didn't exist in the S3 bucket. A curl request to https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html returned 404.
Technical Implementation
File Structure and Content Strategy
The proposal page consolidates multiple concerns into a single self-contained HTML document:
- Pricing Options: Two clearly differentiated charter packages (Option A: 2-hour, $750; Option B: 3-hour full crew, $1,575) to minimize decision fatigue
- Legal Compliance: Explicit disclaimers about USCG licensing and EPA/MPRSA ash scattering regulations
- Brand Content: Historical context linking the vessel to classic Hollywood (Bogart, Bacall, Flynn, Wayne era)
- CTA Form: Q&A contact form at page footer for conversion
The file uses semantic HTML5 markup with inline CSS for portability—critical for proposal documents that may be emailed or printed.
Email Handling and Contact Management
An earlier iteration of the page contained specific contact information tied to an individual. During development, this was replaced with a generic business email (bookings@queenofsandiego.com) routed through the main website's contact infrastructure. This decouples personnel changes from proposal maintenance and allows email forwarding rules to be managed at the application level rather than hardcoded in HTML.
Deployment Pipeline
Local Development Workflow
# 1. File creation in local repo
$ cd /Users/cb/Documents/repos/sites/queenofsandiego.com
$ cat > proposals/jada-charter-proposal-sue.html << 'EOF'
[HTML content]
EOF
# 2. Verification before upload
$ find proposals/ -type f | sort
proposals/jada-charter-proposal-sue.html
AWS Deployment
The deployment leverages two AWS services:
- S3 Bucket:
queenofsandiego.com(primary static asset store) - CloudFront Distribution: Caching layer with automatic invalidation on deploy
# Load environment variables from secrets store
$ set -a; source /Users/cb/Documents/repos/.secrets/repos.env; set +a
# Upload to S3 (single file)
$ 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"
# Verify upload
$ aws s3api head-object \
--bucket queenofsandiego.com \
--key proposals/jada-charter-proposal-sue.html
Cache Invalidation
S3 alone doesn't guarantee immediate edge delivery. CloudFront acts as the origin-facing cache layer, distributing content across global edge locations. Because this is a new file, the cache miss is automatic, but we still trigger an explicit invalidation to ensure the CloudFront metadata is updated immediately:
# Create invalidation batch
$ aws cloudfront create-invalidation \
--distribution-id [DISTRIBUTION_ID] \
--paths "/proposals/jada-charter-proposal-sue.html"
# Monitor invalidation status
$ aws cloudfront get-invalidation \
--distribution-id [DISTRIBUTION_ID] \
--id [INVALIDATION_ID]
The distribution ID is stored in the secrets environment file and loaded at deploy time. CloudFront typically completes invalidations within 60 seconds, after which the file is cached at all edge locations globally.
Architecture Decisions
Why Self-Contained HTML Over a CMS Template
This proposal is a static artifact, not a dynamic view. A proposal document should be:
- Immutable: Once shared, the URL should resolve to the exact content the recipient saw
- Portable: Easily emailed, printed, or archived without server-side dependencies
- Fast: No database queries or template rendering on request
Inline CSS ensures the page renders identically in all contexts (email clients, browsers, PDF converters).
Why CloudFront Invalidation for New Files
While new files automatically miss the cache, explicit invalidation patterns ensure:
- CloudFront metadata is immediately consistent across all edge locations
- Any stale entries from previous failed deployments are cleared
- Deployment scripts have deterministic, observable behavior
Email Abstraction Pattern
Hardcoding email addresses in static HTML creates maintenance debt. The pattern used here—generic business email with backend routing—allows:
- Personnel changes without redeploying HTML
- Email forwarding and filtering at application level
- Audit trails on who actually handles inquiries
- Easier GDPR/privacy compliance (no personal data in static files)
Verification Steps
After deployment, verification occurs at three levels:
- S3 Head Check: Confirms object exists and has correct metadata
- CloudFront Propagation: Validates invalidation is in progress
- HTTP Live Request:
curl -s https://queenofsandiego.com/proposals/jada-charter-proposal-sue.html | grep -E "[pattern]"confirms content is accessible and parseable
The live curl response confirmed HTML structure was intact and key sections (pricing, legal disclaimers, contact form) were present.
What's Next
Pending: Asset integration (hero images, vessel photos, QR codes) require local file paths. Once provided, these will be:
- Copied to
/Users/cb/Documents/repos/sites/queenofsandiego.com/assets/images/proposals/ - Uploaded to S3 with appropriate cache headers
- Referenced in HTML via relative or CDN paths
- Validated for size (CloudFront has no size limits, but optimal is <100KB per image for web)
Monitoring: CloudFront distribution logs can be analyzed to track proposal page views and geographic distribution via standard S3 access logs enabled on the bucket.